From a591d6754af2634e977510ae35405ef4a3fb5b90 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 21:34:43 +0100 Subject: [PATCH 01/19] fix(lint): clear the secret scanners and HTML linters for the org Go workflow Betterleaks, Secretlint, HTMLHint and djLint failed the organization's required Go workflow on test fixtures, not on secret material: - build the Slack-webhook fixture in two pieces, as the test already does for its other secret-shaped fixtures - mark the base64 fixture credentials inline with betterleaks:allow - move the S3 stub's refusal-only credential into a variable whose line carries a Secretlint disable for its basic-auth rule - add a repository Betterleaks config with three allowlists, each bound to one rule and one non-secret shape: numeric docker uid:gid, the mail tests' `fixture:` account, and go.sum `h1:` lines - exclude the byte-exact Hubble UI fixture, whose test requires it unaltered, from the two HTML linters only Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- .betterleaks.toml | 23 +++++++++++++++++++ .mega-linter.yml | 6 +++++ .../credentials_test.go | 6 +++-- ...-trivyignore-configmap-content-boundary.sh | 4 ++-- .../test-verify-wedding-backup-denial.sh | 7 ++++-- 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 .betterleaks.toml diff --git a/.betterleaks.toml b/.betterleaks.toml new file mode 100644 index 0000000000..4d6b6987c5 --- /dev/null +++ b/.betterleaks.toml @@ -0,0 +1,23 @@ +# Betterleaks configuration. MegaLinter's REPOSITORY_BETTERLEAKS extends this file, and this file +# extends the upstream default ruleset. Each allowlist below names one rule and one shape that is +# not secret material, so a real credential anywhere else, or of another shape, still fails. +# Test fixtures that can carry a comment use an inline `betterleaks:allow` instead. + +[extend] +useDefault = true + +[[allowlists]] +description = "Container identities: `docker run --user :` near a curl call is a numeric user, not a curl credential." +targetRules = ["curl-auth-user"] +regexes = ['''^[0-9]+:[0-9]+$'''] + +[[allowlists]] +description = "Disposable test accounts: the mail-server tests sign in as `fixture` to a throwaway local container." +targetRules = ["curl-auth-user"] +regexes = ['''^fixture:[a-z-]+$'''] + +[[allowlists]] +description = "Go module checksums: an `h1:` go.sum line, as quoted in a vendored go.sum diff, is a public hash." +targetRules = ["generic-api-key"] +regexTarget = "line" +regexes = ['''^[+ -]?\S+ v\S+ h1:[A-Za-z0-9+/]{43}=$'''] diff --git a/.mega-linter.yml b/.mega-linter.yml index 50ad20631f..8bb226595c 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -353,6 +353,12 @@ SPELL_CSPELL_FILTER_REGEX_EXCLUDE: (k8s/.*|\.agents/.*) SPELL_LYCHEE_FILTER_REGEX_EXCLUDE: (k8s/.*|\.agents/.*) MARKDOWN_MARKDOWNLINT_FILTER_REGEX_EXCLUDE: \.agents/.* +# The only HTML file here is a byte-exact copy of the released Hubble UI v0.13.6 entry point, which +# check-hubble-ui-streaming's regression test reads and requires unaltered, minifier output included. +# Restyling it would test a page upstream never shipped, so the HTML linters skip that one fixture. +HTML_DJLINT_FILTER_REGEX_EXCLUDE: scripts/check-hubble-ui-streaming/testdata/frontend-v0\.13\.6-index\.html +HTML_HTMLHINT_FILTER_REGEX_EXCLUDE: scripts/check-hubble-ui-streaming/testdata/frontend-v0\.13\.6-index\.html + # The generic YAML tooling is scoped off the manifest tree, and kubeconform is what governs it # instead. That tree is owned by kustomize, Flux and ksail and holds vendored upstream operator # manifests: prettier rewrote kubevirt-operator.yaml across 16,493 lines and CI committed it. diff --git a/scripts/prove-kyverno-cache-recovery/credentials_test.go b/scripts/prove-kyverno-cache-recovery/credentials_test.go index bcf37bec7d..b0e4e80cdc 100644 --- a/scripts/prove-kyverno-cache-recovery/credentials_test.go +++ b/scripts/prove-kyverno-cache-recovery/credentials_test.go @@ -22,7 +22,8 @@ func credentialSecret(t *testing.T, config string, change func(map[string]any)) func TestRegistryPrecheckUsesActualKyvernoCredential(t *testing.T) { for _, config := range []string{ `{"auths":{"ghcr.io":{"username":"probe","password":"private-test-token"}}}`, - `{"auths":{"ghcr.io":{"auth":"cHJvYmU6cHJpdmF0ZS10ZXN0LXRva2Vu"}}}`, + // base64("probe:private-test-token"), the fixture credential above. + `{"auths":{"ghcr.io":{"auth":"cHJvYmU6cHJpdmF0ZS10ZXN0LXRva2Vu"}}}`, // betterleaks:allow } { username, password, err := registryCredential(credentialSecret(t, config, nil)) if err != nil || username != "probe" || password != "private-test-token" { @@ -34,7 +35,8 @@ func TestRegistryPrecheckUsesActualKyvernoCredential(t *testing.T) { func TestAmbiguousOrForeignCredentialCannotQualifyUnsignedControl(t *testing.T) { for _, config := range []string{ `{"auths":{"other.example":{"username":"probe","password":"private-test-token"}}}`, - `{"auths":{"ghcr.io":{"username":"probe","password":"private-test-token","auth":"b3RoZXI6dG9rZW4="}}}`, + // base64("other:token"), a conflicting fixture credential. + `{"auths":{"ghcr.io":{"username":"probe","password":"private-test-token","auth":"b3RoZXI6dG9rZW4="}}}`, // betterleaks:allow `{"auths":{"ghcr.io":{"username":"probe","password":""}}}`, `{"auths":{"ghcr.io":{"username":"probe","password":"private-test-token","password":"changed"}}}`, `{"auths":{"ghcr.io":{"username":"probe","password":"private-test-token"}}} {}`, diff --git a/scripts/tests/test-trivyignore-configmap-content-boundary.sh b/scripts/tests/test-trivyignore-configmap-content-boundary.sh index 318a389d8f..2546113255 100755 --- a/scripts/tests/test-trivyignore-configmap-content-boundary.sh +++ b/scripts/tests/test-trivyignore-configmap-content-boundary.sh @@ -45,7 +45,7 @@ readonly ISSUED_PREFIX_RE='^[[:space:]]*[A-Za-z0-9_.-]+:[[:space:]]*"?(gh[pousr] # ANCHORED AT BOTH ENDS, with the path segments restricted to the lowercase class an image # reference is actually limited to. Matching only a host-like PREFIX excluded any credential that # merely begins host-like, which is a fail-open in the guard rather than a cosmetic gap: -# `hooks.slack.com/services/T00000000/B00000000/AbCdEfGhIjKlMnOpQrStUvWx` matched both expressions, +# a Slack webhook (`hooks.slack.com/services/T00000000/B00000000/…`) matched both expressions, # so a live Slack webhook added to an excepted ConfigMap would be discarded before the opaque-value # check and KSV-01010 would stay suppressed over it. Measured 2026-08-30; the # "hostname-prefixed credential" fixture below pins it. @@ -246,7 +246,7 @@ check_fixture no no "k8s registry image reference" 'registry.k8s.io/kube-state-m # yes), which is what distinguishes it from the two image references above; the uppercase path # segments are what an image reference may not contain. check_fixture yes no "hostname-prefixed credential (slack webhook)" \ - 'hooks.slack.com/services/T00000000/B00000000/AbCdEfGhIjKlMnOpQrStUvWx' + "$(mk 'hooks.slack' '.com/services/T00000000/B00000000/AbCdEfGhIjKlMnOpQrStUvWx')" # The case the naive fix (dropping `/` from the value class) would have broken: a standard-base64 # credential containing `/` must still be caught. diff --git a/scripts/tests/test-verify-wedding-backup-denial.sh b/scripts/tests/test-verify-wedding-backup-denial.sh index 1e404937a1..f302442db8 100755 --- a/scripts/tests/test-verify-wedding-backup-denial.sh +++ b/scripts/tests/test-verify-wedding-backup-denial.sh @@ -424,11 +424,13 @@ if [[ -n "${DENIAL_POD_RUNTIME_IMAGE:-}" ]]; then (cd "${root_dir}" && CGO_ENABLED=0 GOOS=linux go build -o "${real}/s3-access-denied-stub" ./scripts/tests/s3-access-denied-stub) chmod -R a+rwX "${real}" real_rc=0 + # A credential the local stub refuses by design; it unlocks nothing. + stub_endpoint='http://REFUSEDBYSTUB:refused-by-stub-secret@127.0.0.1:9000' # secretlint-disable-line @secretlint/secretlint-rule-basicauth # shellcheck disable=SC2016 # expanded by the container's shell docker run --rm --network none --read-only --user 65532:65532 \ --cap-drop ALL --security-opt no-new-privileges --tmpfs /tmp:rw,size=8m \ --entrypoint /tools/sh -v "${real}:${real}" -e "OUT=${real}" -e MC_CONFIG_DIR=/tmp/mc \ - -e 'MC_HOST_dedicated=http://REFUSEDBYSTUB:refused-by-stub-secret@127.0.0.1:9000' \ + -e "MC_HOST_dedicated=${stub_endpoint}" \ "${DENIAL_POD_RUNTIME_IMAGE}" -c ' "${OUT}/s3-access-denied-stub" 127.0.0.1:9000 "${OUT}/ready" "${OUT}/requests" 2>"${OUT}/stub.err" & i=0 @@ -565,7 +567,8 @@ new_case() { status:{readyInstances:3,conditions:[{type:"Ready",status:"True"},{type:"ContinuousArchiving",status:"True"}]}}' >"${CASE}/cluster.json" store platform-backups coroot-db-backup-r2 | jq '.spec.configuration.destinationPath="s3://platform-backups/cnpg/coroot-db"' >"${CASE}/store-coroot-db.json" store wedding-db-backups wedding-db-backup-r2-dedicated >"${CASE}/store-wedding-db-dedicated.json" - jq -n '{kind:"Secret",metadata:{name:"wedding-db-backup-r2-dedicated",namespace:"wedding-app",uid:"source-uid",resourceVersion:"17"},data:{ACCESS_KEY_ID:"ZGVkaWNhdGVkLWlk",SECRET_ACCESS_KEY:"ZGVkaWNhdGVkLXNlY3JldA==",REGION:"YXV0bw=="}}' >"${CASE}/dedicated-secret.json" + # The data values are base64 of "dedicated-id", "dedicated-secret" and "auto". + jq -n '{kind:"Secret",metadata:{name:"wedding-db-backup-r2-dedicated",namespace:"wedding-app",uid:"source-uid",resourceVersion:"17"},data:{ACCESS_KEY_ID:"ZGVkaWNhdGVkLWlk",SECRET_ACCESS_KEY:"ZGVkaWNhdGVkLXNlY3JldA==",REGION:"YXV0bw=="}}' >"${CASE}/dedicated-secret.json" # betterleaks:allow printf 'Succeeded' >"${CASE}/phase" cp "${work_dir}/passing-pod.log" "${CASE}/pod.log" } From 128387ac008f269772f10d69792cfeb914566192 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 21:56:34 +0100 Subject: [PATCH 02/19] test(go): pass the org workflow's plain and coverage test runs The organization Go workflow runs `go test ./...` and `go test -race -coverprofile=... ./...` with Go's defaults, unlike platform CI's `-parallel 8 -timeout 20m`. refresh-flux-ghcr-auth spends much of each case waiting on the helper's shell and fake-command processes, so the default one slot per CPU took 592s on a four-CPU runner and its last cases ran past the command deadlines derived from Go's ten-minute package budget. TestMain now raises only that default to the eight slots CI already uses; an explicit -parallel is kept as given. TestObservationCommandRefusesForeignDispatchBeforeAccess replaced the child's environment, dropping the GOCOVERDIR a coverage run exports, so the instrumented child's exit warning broke the exact-output check. It is now forwarded, which also records the child's coverage. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- .../refresh-flux-ghcr-auth/fixture_test.go | 28 +++++++++++++++++++ .../observation_test.go | 6 ++++ 2 files changed, 34 insertions(+) diff --git a/scripts/tests/refresh-flux-ghcr-auth/fixture_test.go b/scripts/tests/refresh-flux-ghcr-auth/fixture_test.go index 42b0424340..1d44ee4633 100644 --- a/scripts/tests/refresh-flux-ghcr-auth/fixture_test.go +++ b/scripts/tests/refresh-flux-ghcr-auth/fixture_test.go @@ -8,6 +8,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "flag" "fmt" "os" "os/exec" @@ -88,12 +89,39 @@ func TestMain(m *testing.M) { code = fakeGh(os.Args[1:]) default: cleanup := prepareFakeCommandBinary() + raiseDefaultParallelism() code = m.Run() cleanup() } os.Exit(code) } +// minimumParallelCases is the case concurrency the complete suite is measured +// and reviewed at (#5558). A case spends much of its wall time waiting on the +// helper's shell and fake-command processes rather than on a CPU, so Go's +// default of one slot per CPU leaves a four-CPU runner idle: the suite took +// 407s there at that default and 245s at eight slots. At the default a plain +// `go test ./...` also ran past Go's ten-minute package budget (592s, its last +// cases failing the command deadlines derived from that budget). +const minimumParallelCases = 8 + +// raiseDefaultParallelism lifts only Go's per-CPU default. An explicit +// -parallel, including the eight slots platform CI passes, is kept as given. +func raiseDefaultParallelism() { + flag.Parse() + explicit := false + flag.Visit(func(f *flag.Flag) { + explicit = explicit || f.Name == "test.parallel" + }) + parallel := flag.Lookup("test.parallel") + if explicit || parallel == nil { + return + } + if current, err := strconv.Atoi(parallel.Value.String()); err == nil && current < minimumParallelCases { + _ = parallel.Value.Set(strconv.Itoa(minimumParallelCases)) + } +} + // The fakes are this same binary re-executed under another name, and the script // under test calls them hundreds of times per case. That makes every build flag // applied to the test binary a per-invocation cost: under the coverage diff --git a/scripts/verify-email-data-recovery/observation_test.go b/scripts/verify-email-data-recovery/observation_test.go index 2ecb6bd99b..d4d293900b 100644 --- a/scripts/verify-email-data-recovery/observation_test.go +++ b/scripts/verify-email-data-recovery/observation_test.go @@ -111,6 +111,12 @@ func TestObservationCommandRefusesForeignDispatchBeforeAccess(t *testing.T) { } cmd := exec.Command(os.Args[0], "-test.run=^TestObservationCommandRefusesForeignDispatchBeforeAccess$") cmd.Env = []string{"EMAIL_OBSERVATION_CHILD=1", "PATH=/nonexistent"} + // A coverage run exports GOCOVERDIR. Without it the instrumented child + // warns on stderr at exit, which this exact-output check would read as + // leaked output; forwarding it keeps the check exact and records coverage. + if dir, ok := os.LookupEnv("GOCOVERDIR"); ok { + cmd.Env = append(cmd.Env, "GOCOVERDIR="+dir) + } output, err := cmd.CombinedOutput() exit, ok := err.(*exec.ExitError) if !ok || exit.ExitCode() != 2 || string(output) != "EMAIL_RECOVERY_OBSERVATION=UNVERIFIED stage=dispatch\n" { From ab79eb0d13408bdd1efa7df847ebbfaf151cbcc6 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 22:02:17 +0100 Subject: [PATCH 03/19] fix(go): resolve golangci-lint, deadcode and revive findings The organization Go workflow lints the whole root module with golangci-lint's default configuration, runs `deadcode -test ./...`, and runs revive over every Go file through MegaLinter. golangci-lint (61 findings, 27 shown under its default per-text cap): - errcheck: handle or explicitly discard Close and write errors. A failed verdict write in analyze-cri-logs and observe-crossplane-recovery now refuses instead of reporting success; validate-homepage-bookmarks documents that its buffered writer's Flush decides; tests check json.Unmarshal. - staticcheck: lowercase two error strings, name the shared sentinel errRefused, and apply the De Morgan and tagged-switch rewrites that `--fix` would otherwise leave as uncommitted changes. - unused/deadcode: remove the unreachable containsLine and number. revive: rename unused parameters to _, stop shadowing builtins, put context.Context first, document the exported API of the internal packages, and give each package that had none a package comment. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- internal/backupcatalogue/catalogue.go | 3 + internal/backupcatalogue/deployment_test.go | 2 +- internal/backupcatalogue/storage_test.go | 2 +- internal/backupcatalogue/transport.go | 7 ++ internal/longhorninventory/block_layout.go | 4 + internal/longhorninventory/block_wire.go | 6 ++ internal/longhorninventory/native.go | 5 ++ internal/longhorninventory/replicas.go | 12 +++ internal/longhorninventory/storage_root.go | 10 ++- internal/longhornmigration/pools.go | 8 ++ internal/namespaceinventory/credentials.go | 9 +- internal/namespaceinventory/inventory.go | 7 +- scripts/analyze-cri-logs/main.go | 13 ++- scripts/audit-storage-retention/main.go | 2 + scripts/check-hubble-ui-streaming/main.go | 4 +- scripts/expand-openbao-audit/main.go | 14 +-- scripts/gateway-receiver-binding/main.go | 2 + .../gateway-receiver-binding/provider_test.go | 6 +- .../gateway-receiver-binding/selected_test.go | 2 +- scripts/gateway-receiver-binding/state.go | 4 +- scripts/gateway-receiver-binding/verify.go | 2 +- scripts/guard-cilium-sni/main.go | 2 + scripts/maintain-longhorn-clients/main.go | 2 + scripts/maintain-longhorn-clients/plan.go | 10 +-- .../command_runtime_test.go | 2 +- scripts/migrate-longhorn-v2/main.go | 1 + .../migrate-longhorn-v2/temporary_cordon.go | 6 +- scripts/observe-crossplane-recovery/main.go | 9 +- .../main_test.go | 2 +- .../main.go | 2 + .../openbao-backup-recovery/collector_test.go | 4 +- .../openbao-backup-recovery/transport_test.go | 4 +- scripts/private-mail-config/bootstrap_test.go | 2 +- .../controllers_test.go | 20 ++--- scripts/prove-kyverno-cache-recovery/main.go | 2 + .../service_test.go | 16 ++-- scripts/prove-longhorn-ui-readonly/main.go | 2 + scripts/prove-scoped-package-access/main.go | 2 + .../prove-scoped-package-access/main_test.go | 4 +- .../reconcile-talos-kernel-args/main_test.go | 4 +- scripts/recover-private-mail-probes/main.go | 6 +- .../cluster_transport_test.go | 2 +- .../quarantine_test.go | 10 ++- scripts/retire-arc-pool/transaction.go | 2 +- scripts/retire-arc-pool/writer_test.go | 20 +++-- .../tests/contract-probe-image/native_test.go | 12 +-- .../reconcile_test.go | 2 +- .../tests/fixtures/coroot-dns-probe/main.go | 2 +- .../openbao-transport-runtime/tls_test.go | 6 +- .../historical_node_phase_test.go | 32 +++---- .../project_scope_test.go | 2 +- .../webapp-compiler/tenant_ownership_test.go | 2 +- scripts/tests/webapp-compiler/tenant_test.go | 2 +- scripts/validate-break-glass/main.go | 2 + scripts/validate-dr-signing/main.go | 10 +-- scripts/validate-feature-flags/main.go | 2 + scripts/validate-flux-force-safety/main.go | 2 + scripts/validate-homepage-bookmarks/main.go | 34 +++---- .../main.go | 18 ++-- .../validate-kubescape-frameworks/shell.go | 2 +- .../flux_bridge_test.go | 4 +- scripts/validate-policy-actions/main.go | 2 + .../main.go | 2 + .../main_test.go | 2 +- scripts/verify-arc-app-identity/config.go | 2 +- .../verify-arc-app-identity/runner_group.go | 2 +- .../runner_group_test.go | 8 +- .../verify-arc-app-identity/transport_test.go | 2 +- scripts/verify-arc-app-identity/verify.go | 4 +- .../mailbox_test.go | 6 +- scripts/verify-email-data-recovery/main.go | 2 + .../verify-email-data-recovery/native_test.go | 4 +- .../observation_lifecycle_test.go | 2 +- .../observation_test.go | 2 +- .../verify-email-data-recovery/proof_test.go | 24 ++--- .../verify-email-data-recovery/source_test.go | 12 +-- .../verify-email-provider/lifecycle_test.go | 7 +- scripts/verify-email-storage-canary/main.go | 2 + .../verify-email-storage-canary/proof_test.go | 8 +- .../main.go | 2 + .../verify-wedding-backup-bootstrap/main.go | 40 +++++---- .../operator.go | 26 +++--- .../verify-wedding-backup-bootstrap/proof.go | 6 +- .../runtime.go | 71 ++++++++------- .../verify-wedding-dedicated-restore/main.go | 90 ++++++++++--------- 85 files changed, 418 insertions(+), 304 deletions(-) diff --git a/internal/backupcatalogue/catalogue.go b/internal/backupcatalogue/catalogue.go index 8d9fc8c7ba..726a3e1c0e 100644 --- a/internal/backupcatalogue/catalogue.go +++ b/internal/backupcatalogue/catalogue.go @@ -22,6 +22,9 @@ type catalogueEntry struct { Snapshot snapshot.Manifest Raw json.RawMessage } + +// Catalogue is the strictly loaded snapshot-manifest metadata of one +// repository, serialized only as counts and digests. type Catalogue struct { MetadataOnly bool `json:"metadataOnly"` Snapshots int `json:"snapshots"` diff --git a/internal/backupcatalogue/deployment_test.go b/internal/backupcatalogue/deployment_test.go index 33733bd7cb..732052457b 100644 --- a/internal/backupcatalogue/deployment_test.go +++ b/internal/backupcatalogue/deployment_test.go @@ -38,7 +38,7 @@ func TestBoundObservationDiscardsChangedOrIncompleteBindings(t *testing.T) { } return docs, nil } - read := func(ctx context.Context, p Policy, access, secret, password, parent string, base http.RoundTripper) (Catalogue, error) { + read := func(_ context.Context, p Policy, access, secret, password, _ string, _ http.RoundTripper) (Catalogue, error) { observed++ if p.Prefix != "velero/prod/kopia/wedding-app/" || access != "fixture-access" || secret != "fixture-secret" || password != "fixture-password" { t.Fatal("wrong read scope") diff --git a/internal/backupcatalogue/storage_test.go b/internal/backupcatalogue/storage_test.go index 96215fb9ba..d56d74dd69 100644 --- a/internal/backupcatalogue/storage_test.go +++ b/internal/backupcatalogue/storage_test.go @@ -106,7 +106,7 @@ func TestStorageOnlyAcceptsValidatedOptionalMissingBlob(t *testing.T) { func TestStorageRejectsInvalidRangesWithoutReading(t *testing.T) { for _, tc := range []struct{ offset, length int64 }{{-1, -1}, {1, -1}, {0, -2}, {1 << 62, 1 << 62}} { - s, err := newStorage(t.Context(), testPolicy(), "access", "secret", roundTripFunc(func(r *http.Request) (*http.Response, error) { + s, err := newStorage(t.Context(), testPolicy(), "access", "secret", roundTripFunc(func(_ *http.Request) (*http.Response, error) { t.Fatal("invalid range reached backend") return nil, nil })) diff --git a/internal/backupcatalogue/transport.go b/internal/backupcatalogue/transport.go index e2e94f617c..572f9c34b5 100644 --- a/internal/backupcatalogue/transport.go +++ b/internal/backupcatalogue/transport.go @@ -18,6 +18,8 @@ import ( "time" ) +// Policy fixes the one R2 bucket prefix a ReadTransport may read and its +// request, total-byte and per-response byte budgets. type Policy struct { Endpoint, Bucket, Prefix string MaxRequests int @@ -53,6 +55,8 @@ type ReadTransport struct { operations int } +// NewReadTransport validates p and wraps base in a read-only transport bounded +// by ctx and a ten-minute ceiling. func NewReadTransport(ctx context.Context, p Policy, base http.RoundTripper) (*ReadTransport, error) { u, err := url.Parse(p.Endpoint) if ctx == nil || base == nil || err != nil || u.Scheme != "https" || !r2Host.MatchString(u.Host) || @@ -166,6 +170,8 @@ func (g *ReadTransport) signalLocked() { g.changed = make(chan struct{}) } +// RoundTrip forwards an in-scope GET or HEAD within the policy budgets. Any +// refused or failed request holds every later one. func (g *ReadTransport) RoundTrip(r *http.Request) (*http.Response, error) { if !g.allowed(r) || !contextActive(r.Context()) { return nil, g.hold() @@ -282,6 +288,7 @@ func (g *ReadTransport) Err() error { return nil } +// Close cancels the transport context, ending every outstanding request. func (g *ReadTransport) Close() { g.cancel() } func (g *ReadTransport) beginOperation(ctx context.Context) error { diff --git a/internal/longhorninventory/block_layout.go b/internal/longhorninventory/block_layout.go index 9aa98b5486..6104e746d3 100644 --- a/internal/longhorninventory/block_layout.go +++ b/internal/longhorninventory/block_layout.go @@ -13,6 +13,8 @@ import ( "github.com/siderolabs/talos/pkg/machinery/resources/block" ) +// BlockLayoutReport summarizes the EPHEMERAL and raw Longhorn volumes' phase, +// size and encryption properties, without device paths or identities. type BlockLayoutReport struct { VolumeStatuses, DiscoveredVolumes int EphemeralPhase string @@ -26,6 +28,8 @@ type BlockLayoutReport struct { Digest string } +// ListBlockLayout reads the block layout twice and reports it only when both +// complete reads agree. func (n Native) ListBlockLayout(ctx context.Context) (BlockLayoutReport, error) { ctx, cancel, err := n.requestContext(ctx) if err != nil { diff --git a/internal/longhorninventory/block_wire.go b/internal/longhorninventory/block_wire.go index be5ba7fa9d..965d2a27af 100644 --- a/internal/longhorninventory/block_wire.go +++ b/internal/longhorninventory/block_wire.go @@ -16,19 +16,25 @@ import ( "google.golang.org/protobuf/proto" ) +// BlockCollections lists Talos block resources of one kind. type BlockCollections interface { List(context.Context, resource.Kind, ...state.ListOption) (resource.List, error) } +// BlockListAPI is the COSI resource-list stream BoundedBlocks reads. type BlockListAPI interface { List(context.Context, *v1alpha1.ListRequest, ...grpc.CallOption) (grpc.ServerStreamingClient[v1alpha1.ListResponse], error) } +// BoundedBlocks is a BlockCollections limited to the block namespace's volume +// status and discovered-volume kinds on the one node named by Target. type BoundedBlocks struct { API BlockListAPI Target string } +// List reads one bounded collection, refusing any other kind, option or node, +// and any deadline missing or more than 15 seconds away. func (r BoundedBlocks) List(ctx context.Context, kind resource.Kind, options ...state.ListOption) (resource.List, error) { invalid := nativeFailure{code: "block-list"} if ctx == nil || !active(ctx) || r.API == nil || kind == nil || len(options) != 0 || kind.Namespace() != block.NamespaceName { diff --git a/internal/longhorninventory/native.go b/internal/longhorninventory/native.go index c52ca2fa3d..c81dd3a1eb 100644 --- a/internal/longhorninventory/native.go +++ b/internal/longhorninventory/native.go @@ -19,13 +19,17 @@ import ( "google.golang.org/protobuf/proto" ) +// ReplicaRoot is the fixed V1 replica directory ListReplicas observes. const ReplicaRoot = "/var/lib/longhorn/replicas" +// Files is the subset of the Talos machine API the observer uses. type Files interface { List(context.Context, *machine.ListRequest, ...grpc.CallOption) (grpc.ServerStreamingClient[machine.FileInfo], error) Read(context.Context, *machine.ReadRequest, ...grpc.CallOption) (grpc.ServerStreamingClient[common.Data], error) } +// Native observes one node, addressed by its IP in Target, through the Talos +// file and block-resource APIs. type Native struct { API Files Target string @@ -55,6 +59,7 @@ func active(ctx context.Context) bool { return bounded && ctx.Err() == nil && time.Now().Before(deadline) } +// ListReplicas lists the immediate children of ReplicaRoot. func (n Native) ListReplicas(ctx context.Context) (Listing, error) { return n.listFixedRoot(ctx, ReplicaRoot) } diff --git a/internal/longhorninventory/replicas.go b/internal/longhorninventory/replicas.go index f8e6c75e7d..7f17442d8f 100644 --- a/internal/longhorninventory/replicas.go +++ b/internal/longhorninventory/replicas.go @@ -1,3 +1,6 @@ +// Package longhorninventory observes a Talos node's Longhorn storage through +// read-only native Talos APIs and reduces it to counters and commitments. It +// never reads file contents, follows links or reports private identities. package longhorninventory import ( @@ -11,11 +14,14 @@ import ( "unicode/utf8" ) +// Bounds on one observation: a listing or replica set larger than either is +// refused rather than truncated. const ( MaxEntries = 16384 MaxMetadataBytes = 4 << 20 ) +// Entry is the metadata of one immediate child of an observed directory. type Entry struct { Name string Mode uint32 @@ -24,15 +30,21 @@ type Entry struct { Size, Modified int64 } +// Listing is a directory's immediate children; Complete is false when the +// observation ended early. type Listing struct { Entries []Entry Complete bool } +// Replica is one replica identity from the Longhorn API and the data directory +// it owns. type Replica struct { Name, UID, Directory string } +// Report reconciles the replica directory with the API's replicas as counts +// and a digest; UnmatchedNames is never serialized. type Report struct { Directories, Files, Matched, Unmatched, Missing int EmptyAndAccounted bool diff --git a/internal/longhorninventory/storage_root.go b/internal/longhorninventory/storage_root.go index 39a23be9bc..b2146f4672 100644 --- a/internal/longhorninventory/storage_root.go +++ b/internal/longhorninventory/storage_root.go @@ -10,8 +10,11 @@ import ( "strings" ) +// StorageRoot is the fixed Longhorn data root ListStorageRoot observes. const StorageRoot = "/var/lib/longhorn" +// StorageRootReport counts the storage root's children, records which known +// Longhorn directories are present, and commits to the listing by digest. type StorageRootReport struct { Directories, Files, OtherEntries int ReplicaEntryPresent, ReplicaDirectoryPresent bool @@ -19,9 +22,13 @@ type StorageRootReport struct { Digest string } +// ListStorageRoot lists the immediate children of StorageRoot. func (n Native) ListStorageRoot(ctx context.Context) (Listing, error) { return n.listFixedRoot(ctx, StorageRoot) } + +// AnalyzeStorageRoot qualifies a complete storage-root listing and reduces it +// to a StorageRootReport. func AnalyzeStorageRoot(listing Listing) (StorageRootReport, error) { entries, _, err := normalizedEntries(listing) if err != nil { @@ -72,7 +79,8 @@ func (e nativeFailure) Error() string { } func (nativeFailure) Unwrap() error { return errNative } -// Only package-authored classifications enter logs. Transport messages and +// FailureCode returns the package-authored class of an observation failure, +// or "unknown". Only these classifications enter logs. Transport messages and // file-error strings never select a diagnostic class or grant missing-path proof. func FailureCode(err error) string { var failure nativeFailure diff --git a/internal/longhornmigration/pools.go b/internal/longhornmigration/pools.go index eb47fb2676..f12a8db1a1 100644 --- a/internal/longhornmigration/pools.go +++ b/internal/longhornmigration/pools.go @@ -11,8 +11,11 @@ import ( "sigs.k8s.io/yaml" ) +// Object is a decoded JSON or YAML object. type Object = map[string]any +// Labels and annotations that mark migration-pool nodes, Longhorn storage +// authorization and the owned scale-down protection. const ( PoolLabel = "storage.platform.devantler.tech/migration-pool" EnabledLabel = "storage.platform.devantler.tech/migration-enabled" @@ -21,6 +24,7 @@ const ( ScaleDownDisabled = "cluster-autoscaler.kubernetes.io/scale-down-disabled" ) +// Pool is one reviewed fixed-size migration pool from the cluster config. type Pool struct { Name, Role, ServerType string Count int @@ -58,6 +62,8 @@ const ( reviewedLocation = "fsn1" ) +// ConfiguredPools returns the v1 and v2 migration pools from a KSail cluster +// config, refusing any pool, capacity or hardware that differs from the review. func ConfiguredPools(data []byte) (map[string]Pool, error) { b, err := yaml.YAMLToJSONStrict(data) var cfg Object @@ -94,6 +100,8 @@ func ConfiguredPools(data []byte) (map[string]Pool, error) { return result, nil } +// VerifyNode requires a ready, schedulable, autoscaled node whose name, labels, +// instance type and provider identity belong to pool. func VerifyNode(n Object, pool Pool) error { if !regexp.MustCompile("^"+regexp.QuoteMeta(pool.Name)+"-[0-9a-f]{16}$").MatchString(text(n, "metadata", "name")) || text(n, "metadata", "labels", PoolLabel) != pool.Role || text(n, "metadata", "labels", "ksail.io/autoscaled") != "true" || diff --git a/internal/namespaceinventory/credentials.go b/internal/namespaceinventory/credentials.go index 6f6060c715..2ebda6c73c 100644 --- a/internal/namespaceinventory/credentials.go +++ b/internal/namespaceinventory/credentials.go @@ -19,11 +19,11 @@ var credentialPath = regexp.MustCompile(`^/dev/shm/longhorn-namespace-inventory\ var sourcePattern = regexp.MustCompile(`^[0-9a-f]{40}$`) var runPattern = regexp.MustCompile(`^[1-9][0-9]{0,18}$`) -// LoadProtectedClient reads only the private memory-backed credential created -// by the confirmed first-attempt workflow. It never loads an ambient context, -// credential file named by kubeconfig content, or authentication plugin. +// Dispatch is the verified source commit and run of a protected observation. type Dispatch struct{ Source, Run string } +// AuthorizeProtectedDispatch accepts only the confirmed first attempt of the +// observation workflow on main, on a GitHub-hosted runner. func AuthorizeProtectedDispatch(getenv func(string) string) (Dispatch, error) { for key, want := range map[string]string{ "GITHUB_ACTIONS": "true", "GITHUB_EVENT_NAME": "workflow_dispatch", "GITHUB_REF": "refs/heads/main", @@ -41,6 +41,9 @@ func AuthorizeProtectedDispatch(getenv func(string) string) (Dispatch, error) { return Dispatch{getenv("GITHUB_SHA"), getenv("GITHUB_RUN_ID")}, nil } +// LoadProtectedClient reads only the private memory-backed credential created +// by the confirmed first-attempt workflow. It never loads an ambient context, +// credential file named by kubeconfig content, or authentication plugin. func LoadProtectedClient(getenv func(string) string) (*http.Client, *url.URL, error) { if _, err := AuthorizeProtectedDispatch(getenv); err != nil { return nil, nil, err diff --git a/internal/namespaceinventory/inventory.go b/internal/namespaceinventory/inventory.go index af6612b75c..c2e8c1b6ef 100644 --- a/internal/namespaceinventory/inventory.go +++ b/internal/namespaceinventory/inventory.go @@ -13,12 +13,15 @@ import ( "github.com/devantler-tech/platform/internal/strictjson" ) +// Binding is the identity of one observed object. type Binding struct{ Name, Namespace, UID, Kind string } -// Bindings remain private until a caller explicitly copies them into its private -// journal. Default JSON serialization never publishes object identities. +// Inventory holds the observed bindings. They remain private until a caller +// explicitly copies them into its private journal. Default JSON serialization +// never publishes object identities. type Inventory struct{ bindings map[string]Binding } +// Bindings returns a copy of the observed bindings ordered by UID. func (i Inventory) Bindings() []Binding { result := make([]Binding, 0, len(i.bindings)) for _, binding := range i.bindings { diff --git a/scripts/analyze-cri-logs/main.go b/scripts/analyze-cri-logs/main.go index 0c674c03dc..c664f5bfce 100644 --- a/scripts/analyze-cri-logs/main.go +++ b/scripts/analyze-cri-logs/main.go @@ -1,3 +1,6 @@ +// Command analyze-cri-logs is the opt-in offline diagnostic for finite CRI +// log records: it reports request pairing and slow-window counts without +// establishing Coroot request attribution or recovery. package main import ( @@ -293,7 +296,7 @@ func hasGaps(n nodeStats) bool { } func run(args []string, stdout io.Writer) int { - fail := func(message string) int { fmt.Fprintln(stdout, "ANALYSIS=REFUSED "+message); return 2 } + fail := func(message string) int { _, _ = fmt.Fprintln(stdout, "ANALYSIS=REFUSED "+message); return 2 } fs := flag.NewFlagSet("analyze-cri-logs", flag.ContinueOnError) fs.SetOutput(io.Discard) // Flag errors can contain raw paths or values. var logs inputs @@ -341,12 +344,12 @@ func run(args []string, stdout io.Writer) int { } before, err := f.Stat() if err != nil || !before.Mode().IsRegular() || before.Mode().Perm()&0077 != 0 || before.Size() > maxFileBytes { - f.Close() + _ = f.Close() return fail("input must be a private bounded regular file: " + label) } total += before.Size() if total > maxTotalBytes { - f.Close() + _ = f.Close() return fail("total input limit exceeded") } h := sha256.New() @@ -392,7 +395,9 @@ func run(args []string, stdout io.Writer) int { slowPairs += n.SlowPairs failures += n.FailedPairs } - fmt.Fprintf(stdout, "ANALYSIS=%s nodes=%d successful_pairs=%d slow_window_pairs=%d failed_pairs=%d coroot_attribution=UNPROVEN\n", r.Status, len(r.Nodes), pairs, slowPairs, failures) + if _, err := fmt.Fprintf(stdout, "ANALYSIS=%s nodes=%d successful_pairs=%d slow_window_pairs=%d failed_pairs=%d coroot_attribution=UNPROVEN\n", r.Status, len(r.Nodes), pairs, slowPairs, failures); err != nil { + return 2 + } if r.Status == "GAPS" { return 1 } diff --git a/scripts/audit-storage-retention/main.go b/scripts/audit-storage-retention/main.go index 3d873016b9..0c5b8e1eeb 100644 --- a/scripts/audit-storage-retention/main.go +++ b/scripts/audit-storage-retention/main.go @@ -1,3 +1,5 @@ +// Command audit-storage-retention audits a captured, read-only list of the +// cluster's volumes and claims for storage retention, reporting only counts. package main import ( diff --git a/scripts/check-hubble-ui-streaming/main.go b/scripts/check-hubble-ui-streaming/main.go index 4fb3422df0..4a73d44ff9 100644 --- a/scripts/check-hubble-ui-streaming/main.go +++ b/scripts/check-hubble-ui-streaming/main.go @@ -232,7 +232,7 @@ func exercise(ctx context.Context, image, config string, wantSpool bool) (result } } if observed.response != nil { - defer observed.response.Body.Close() + defer func() { _ = observed.response.Body.Close() }() } if observed.err != nil || string(observed.body) != prefix || observed.response.StatusCode != http.StatusOK { return fmt.Errorf("stream prefix or response changed: %v", observed.err) @@ -256,7 +256,7 @@ func exercise(ctx context.Context, image, config string, wantSpool bool) (result if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil || resp.StatusCode != http.StatusServiceUnavailable || string(body) != "fixture upstream unavailable" || resp.Header.Get("X-Fixture-Response") != "preserved" { return fmt.Errorf("upstream failure propagation changed: %v", err) diff --git a/scripts/expand-openbao-audit/main.go b/scripts/expand-openbao-audit/main.go index be51db1632..c7d8c4cb23 100644 --- a/scripts/expand-openbao-audit/main.go +++ b/scripts/expand-openbao-audit/main.go @@ -1,3 +1,5 @@ +// Command expand-openbao-audit inspects, or performs, the approved bounded +// expansion of the OpenBao audit volume, preserving its records. package main import ( @@ -203,9 +205,9 @@ func (e engine) collect(ctx context.Context) (snapshot, error) { if err != nil || !valid(c, "v1", "PersistentVolumeClaim", "openbao", name) || field(c, "status", "phase") != "Bound" || field(c, "spec", "storageClassName") != "hcloud" || field(c, "metadata", "labels", "app.kubernetes.io/name") != "openbao" { return s, claimUnverified } - req, cap := str(field(c, "spec", "resources", "requests", "storage")), str(field(c, "status", "capacity", "storage")) + req, capacity := str(field(c, "spec", "resources", "requests", "storage")), str(field(c, "status", "capacity", "storage")) _, typedConditions := field(c, "status", "conditions").([]any) - if req != "1Gi" && req != "10Gi" && req != target || cap != "10Gi" && cap != target || field(c, "status", "conditions") != nil && !typedConditions { + if req != "1Gi" && req != "10Gi" && req != target || capacity != "10Gi" && capacity != target || field(c, "status", "conditions") != nil && !typedConditions { return s, claimCapacityUnreviewed } pvName := str(field(c, "spec", "volumeName")) @@ -280,14 +282,14 @@ func auditPod(p map[string]any, name, claim, owner string) bool { } func stableResource(r map[string]any, omitCapacity bool) map[string]any { b, _ := json.Marshal(r) - copy, _ := decode(b) - spec := obj(copy["spec"]) + clone, _ := decode(b) + spec := obj(clone["spec"]) if omitCapacity { delete(spec, "capacity") - } else if requests := obj(field(copy, "spec", "resources", "requests")); requests != nil { + } else if requests := obj(field(clone, "spec", "resources", "requests")); requests != nil { delete(requests, "storage") } - return map[string]any{"uid": field(copy, "metadata", "uid"), "name": field(copy, "metadata", "name"), "namespace": field(copy, "metadata", "namespace"), "owners": field(copy, "metadata", "ownerReferences"), "labels": field(copy, "metadata", "labels"), "annotations": field(copy, "metadata", "annotations"), "finalizers": field(copy, "metadata", "finalizers"), "spec": spec} + return map[string]any{"uid": field(clone, "metadata", "uid"), "name": field(clone, "metadata", "name"), "namespace": field(clone, "metadata", "namespace"), "owners": field(clone, "metadata", "ownerReferences"), "labels": field(clone, "metadata", "labels"), "annotations": field(clone, "metadata", "annotations"), "finalizers": field(clone, "metadata", "finalizers"), "spec": spec} } func preserved(a, b snapshot) bool { if !reflect.DeepEqual(stableResource(a.workload, false), stableResource(b.workload, false)) || !reflect.DeepEqual(a.class, b.class) { diff --git a/scripts/gateway-receiver-binding/main.go b/scripts/gateway-receiver-binding/main.go index ceffa6fa3c..b4563541c9 100644 --- a/scripts/gateway-receiver-binding/main.go +++ b/scripts/gateway-receiver-binding/main.go @@ -1,3 +1,5 @@ +// Command gateway-receiver-binding restores, applies and verifies the +// reviewed Gateway receiver state during production deployment. package main import ( diff --git a/scripts/gateway-receiver-binding/provider_test.go b/scripts/gateway-receiver-binding/provider_test.go index ffceb452a3..6980be1d62 100644 --- a/scripts/gateway-receiver-binding/provider_test.go +++ b/scripts/gateway-receiver-binding/provider_test.go @@ -297,9 +297,9 @@ func TestProviderObservationDoesNotFollowRedirectOrDiscloseProviderBody(t *testi if r.URL.Host != "api.hetzner.cloud" { t.Fatal("redirect followed") } - copy := r.Clone(r.Context()) - copy.URL.Scheme, copy.URL.Host = local.Scheme, local.Host - return transport.RoundTrip(copy) + clone := r.Clone(r.Context()) + clone.URL.Scheme, clone.URL.Host = local.Scheme, local.Host + return transport.RoundTrip(clone) })} _, err := observeProvider(context.Background(), client, "disposable-test-token", b, false) if err == nil || strings.Contains(err.Error(), "private-provider-detail") || strings.Contains(err.Error(), "disposable-test-token") || forwarded != 0 { diff --git a/scripts/gateway-receiver-binding/selected_test.go b/scripts/gateway-receiver-binding/selected_test.go index a80fe86122..1debc45355 100644 --- a/scripts/gateway-receiver-binding/selected_test.go +++ b/scripts/gateway-receiver-binding/selected_test.go @@ -412,7 +412,7 @@ func TestInstallFenceWaitsForTheFluxLayerWithoutWriting(t *testing.T) { held := api.bindings api.bindings = map[string][]byte{} pauses := 0 - wrote, err := receiverClient{context: "admin@prod", command: api.command}.installFence(context.Background(), b, func(_ context.Context, d time.Duration) error { + wrote, err := receiverClient{context: "admin@prod", command: api.command}.installFence(context.Background(), b, func(_ context.Context, _ time.Duration) error { pauses++ if pauses <= 3 && len(api.writes) != 0 { t.Fatal("wrote before the bindings arrived") diff --git a/scripts/gateway-receiver-binding/state.go b/scripts/gateway-receiver-binding/state.go index b6de0e7036..4ca7eab638 100644 --- a/scripts/gateway-receiver-binding/state.go +++ b/scripts/gateway-receiver-binding/state.go @@ -113,11 +113,11 @@ func inspectReceiver(raw []byte, b binding) (receiverState, error) { } } } - copy, err := json.Marshal(body) + encoded, err := json.Marshal(body) if err != nil { return bad() } - if _, err = disabledPatch(copy, b); err != nil { + if _, err = disabledPatch(encoded, b); err != nil { return bad() } annotations, _ := meta["annotations"].(map[string]any) diff --git a/scripts/gateway-receiver-binding/verify.go b/scripts/gateway-receiver-binding/verify.go index 728de9cd33..5d8f79c50c 100644 --- a/scripts/gateway-receiver-binding/verify.go +++ b/scripts/gateway-receiver-binding/verify.go @@ -22,7 +22,7 @@ func secretMatches(raw []byte, b binding) bool { func probePublic(ctx context.Context, ip string) error { transport := http.DefaultTransport.(*http.Transport).Clone() transport.Proxy = nil - transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { + transport.DialContext = func(ctx context.Context, _, address string) (net.Conn, error) { host, port, err := net.SplitHostPort(address) if err != nil || port != "443" || (host != "platform.devantler.tech" && host != "vault.platform.devantler.tech") { return nil, errors.New("public-target") diff --git a/scripts/guard-cilium-sni/main.go b/scripts/guard-cilium-sni/main.go index 7f510280cb..044f3f1134 100644 --- a/scripts/guard-cilium-sni/main.go +++ b/scripts/guard-cilium-sni/main.go @@ -1,3 +1,5 @@ +// Command guard-cilium-sni verifies SNI hostname coverage in the committed +// Cilium network policies. package main import ( diff --git a/scripts/maintain-longhorn-clients/main.go b/scripts/maintain-longhorn-clients/main.go index f26f98d4d1..fbc480b15b 100644 --- a/scripts/maintain-longhorn-clients/main.go +++ b/scripts/maintain-longhorn-clients/main.go @@ -1,3 +1,5 @@ +// Command maintain-longhorn-clients changes Longhorn client placement only +// after its volumes are detached and the database clusters are healthy. package main import ( diff --git a/scripts/maintain-longhorn-clients/plan.go b/scripts/maintain-longhorn-clients/plan.go index 672e65d383..08fb0e9eff 100644 --- a/scripts/maintain-longhorn-clients/plan.go +++ b/scripts/maintain-longhorn-clients/plan.go @@ -418,13 +418,13 @@ func withoutField(value any, path []string) any { if !ok || len(path) == 0 { return value } - copy := maps.Clone(m) + clone := maps.Clone(m) if len(path) == 1 { - delete(copy, path[0]) - } else if child, exists := copy[path[0]]; exists { - copy[path[0]] = withoutField(child, path[1:]) + delete(clone, path[0]) + } else if child, exists := clone[path[0]]; exists { + clone[path[0]] = withoutField(child, path[1:]) } - return copy + return clone } // Attachment targets are controller-owned and must change when consumers move. diff --git a/scripts/migrate-longhorn-v2/command_runtime_test.go b/scripts/migrate-longhorn-v2/command_runtime_test.go index 6bb5b1b3bd..71cbac4af7 100644 --- a/scripts/migrate-longhorn-v2/command_runtime_test.go +++ b/scripts/migrate-longhorn-v2/command_runtime_test.go @@ -14,7 +14,7 @@ import ( // This child process exercises the actual pipe, exit and cancellation path. // All content is synthetic; no external command or cluster is contacted. -func TestMigrationCommandFixture(t *testing.T) { +func TestMigrationCommandFixture(_ *testing.T) { if len(os.Args) < 3 || os.Args[len(os.Args)-2] != "--" { return } diff --git a/scripts/migrate-longhorn-v2/main.go b/scripts/migrate-longhorn-v2/main.go index 831a068118..5ab1e103c9 100644 --- a/scripts/migrate-longhorn-v2/main.go +++ b/scripts/migrate-longhorn-v2/main.go @@ -1,3 +1,4 @@ +// Command migrate-longhorn-v2 prepares Longhorn V2 workers. package main import ( diff --git a/scripts/migrate-longhorn-v2/temporary_cordon.go b/scripts/migrate-longhorn-v2/temporary_cordon.go index e5a800ef47..e58fb03c77 100644 --- a/scripts/migrate-longhorn-v2/temporary_cordon.go +++ b/scripts/migrate-longhorn-v2/temporary_cordon.go @@ -27,11 +27,11 @@ func temporaryInventoryNodes(nodes []object, record *journal) ([]object, error) return nil, errInventory } found = true - copy := maps.Clone(node) + clone := maps.Clone(node) spec := maps.Clone(valueObject(node, "spec")) spec["unschedulable"] = false - copy["spec"] = spec - result[i] = copy + clone["spec"] = spec + result[i] = clone } if !found { return nil, errInventory diff --git a/scripts/observe-crossplane-recovery/main.go b/scripts/observe-crossplane-recovery/main.go index d033bb7b93..e9ed2dff22 100644 --- a/scripts/observe-crossplane-recovery/main.go +++ b/scripts/observe-crossplane-recovery/main.go @@ -24,7 +24,7 @@ func allowedDispatch(getEnv func(string) string) bool { } func runObservation(ctx context.Context, getEnv func(string) string, read executor, expected map[string]string, checksum string, now time.Time, out io.Writer) int { - unknown := func() int { fmt.Fprintln(out, "CROSSPLANE_RECOVERY=UNKNOWN"); return 3 } + unknown := func() int { _, _ = fmt.Fprintln(out, "CROSSPLANE_RECOVERY=UNKNOWN"); return 3 } if !allowedDispatch(getEnv) { return unknown() } @@ -51,10 +51,13 @@ func runObservation(ctx context.Context, getEnv func(string) string, read execut return unknown() } if count != 0 { - fmt.Fprintf(out, "CROSSPLANE_RECOVERY=ALERTS_PRESENT peer_alert_records=%d\n", count) + _, _ = fmt.Fprintf(out, "CROSSPLANE_RECOVERY=ALERTS_PRESENT peer_alert_records=%d\n", count) return 1 } - fmt.Fprintln(out, "CROSSPLANE_RECOVERY=RECOVERED") + // An unwritten verdict is not a reported recovery. + if _, err := fmt.Fprintln(out, "CROSSPLANE_RECOVERY=RECOVERED"); err != nil { + return 3 + } return 0 } diff --git a/scripts/observe-historical-longhorn-backups/main_test.go b/scripts/observe-historical-longhorn-backups/main_test.go index d1ebc6b43e..9932d89f59 100644 --- a/scripts/observe-historical-longhorn-backups/main_test.go +++ b/scripts/observe-historical-longhorn-backups/main_test.go @@ -78,7 +78,7 @@ func TestRunNeverPublishesPartialOrStaleMetadata(t *testing.T) { env := authorizedEnvironment() get := func(k string) string { return env[k] } checks, observations := 0, 0 - runner := func(ctx context.Context, program string, args ...string) ([]byte, error) { + runner := func(_ context.Context, program string, args ...string) ([]byte, error) { if program != "git" || !readAllowed(program, args...) { t.Fatal("unexpected command") } diff --git a/scripts/observe-longhorn-namespace-inventory/main.go b/scripts/observe-longhorn-namespace-inventory/main.go index a91f65904e..3162872e88 100644 --- a/scripts/observe-longhorn-namespace-inventory/main.go +++ b/scripts/observe-longhorn-namespace-inventory/main.go @@ -1,3 +1,5 @@ +// Command observe-longhorn-namespace-inventory checks, read-only, whether +// production returns a complete metadata inventory of the Longhorn namespace. package main import ( diff --git a/scripts/openbao-backup-recovery/collector_test.go b/scripts/openbao-backup-recovery/collector_test.go index 0e092ad4dd..ec0b59c01c 100644 --- a/scripts/openbao-backup-recovery/collector_test.go +++ b/scripts/openbao-backup-recovery/collector_test.go @@ -129,9 +129,9 @@ func TestIndependentProductionReaderLimitsKubernetesObjects(t *testing.T) { // TestSnapshotTransportNeverForwardsSignedReads exercises real local HTTP connections. func TestSnapshotTransportNeverForwardsSignedReads(t *testing.T) { var proxied, followed bool - proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { proxied = true; w.WriteHeader(http.StatusNoContent) })) + proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { proxied = true; w.WriteHeader(http.StatusNoContent) })) defer proxy.Close() - destination := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { followed = true; w.WriteHeader(http.StatusOK) })) + destination := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { followed = true; w.WriteHeader(http.StatusOK) })) defer destination.Close() source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, destination.URL, http.StatusTemporaryRedirect) diff --git a/scripts/openbao-backup-recovery/transport_test.go b/scripts/openbao-backup-recovery/transport_test.go index 3d480fac8a..50a9485a4f 100644 --- a/scripts/openbao-backup-recovery/transport_test.go +++ b/scripts/openbao-backup-recovery/transport_test.go @@ -21,7 +21,7 @@ func TestAPIRefusesExternalOriginsRedirectsAndIncompleteHealth(t *testing.T) { } } var followed atomic.Bool - target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { followed.Store(true); w.WriteHeader(http.StatusOK) })) + target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { followed.Store(true); w.WriteHeader(http.StatusOK) })) defer target.Close() source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL, http.StatusTemporaryRedirect) @@ -36,7 +36,7 @@ func TestAPIRefusesExternalOriginsRedirectsAndIncompleteHealth(t *testing.T) { t.Fatal("redirect could expose the recovery token") } for _, body := range []string{`{}`, `{"initialized":true,"sealed":false}`, `{"initialized":true,"sealed":false,"version":"2.5.3"}`} { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(body)) })) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(body)) })) client, err := newAPI(server.URL) if err != nil { t.Fatal(err) diff --git a/scripts/private-mail-config/bootstrap_test.go b/scripts/private-mail-config/bootstrap_test.go index 479f28f033..58098444da 100644 --- a/scripts/private-mail-config/bootstrap_test.go +++ b/scripts/private-mail-config/bootstrap_test.go @@ -57,7 +57,7 @@ func bootstrapInputs(t *testing.T) string { // This actual child process deliberately never opens management. The parent // must stop and collect it when startup fails or cancellation arrives. -func TestOwnedBootstrapChild(t *testing.T) { +func TestOwnedBootstrapChild(_ *testing.T) { if os.Getenv("STALWART_RECOVERY_MODE") != "1" { return } diff --git a/scripts/prove-kyverno-cache-recovery/controllers_test.go b/scripts/prove-kyverno-cache-recovery/controllers_test.go index 67d210f8bb..5d11fe339d 100644 --- a/scripts/prove-kyverno-cache-recovery/controllers_test.go +++ b/scripts/prove-kyverno-cache-recovery/controllers_test.go @@ -56,28 +56,28 @@ func TestRecoveredAdmissionRetainsReplicaAndDisruptionFloors(t *testing.T) { func TestUnavailableOrUnownedAdmissionCannotPass(t *testing.T) { for name, change := range map[string]func(object, object, object, []object){ - "one replica": func(d, p, r object, pods []object) { d["status"].(object)["readyReplicas"] = 1 }, - "foreign executable": func(d, p, r object, pods []object) { + "one replica": func(d, _, _ object, _ []object) { d["status"].(object)["readyReplicas"] = 1 }, + "foreign executable": func(_, _, _ object, pods []object) { pods[0]["spec"].(object)["containers"].([]any)[0].(object)["image"] = "registry.invalid/foreign:latest" }, - "arguments rewritten": func(d, p, r object, pods []object) { + "arguments rewritten": func(_, _, _ object, pods []object) { pods[0]["spec"].(object)["containers"].([]any)[0].(object)["args"] = []any{"--foreign"} }, - "unprotected disruption": func(d, p, r object, pods []object) { p["spec"].(object)["minAvailable"] = 0 }, - "unavailable PDB": func(d, p, r object, pods []object) { p["status"].(object)["disruptionsAllowed"] = 0 }, - "foreign owner": func(d, p, r object, pods []object) { + "unprotected disruption": func(_, p, _ object, _ []object) { p["spec"].(object)["minAvailable"] = 0 }, + "unavailable PDB": func(_, p, _ object, _ []object) { p["status"].(object)["disruptionsAllowed"] = 0 }, + "foreign owner": func(_, _, _ object, pods []object) { pods[0]["metadata"].(object)["ownerReferences"] = owned("ReplicaSet", "foreign-rs") }, - "unready process": func(d, p, r object, pods []object) { + "unready process": func(_, _, _ object, pods []object) { pods[0]["status"].(object)["containerStatuses"].([]any)[0].(object)["ready"] = false }, - "blank process identity": func(d, p, r object, pods []object) { + "blank process identity": func(_, _, _ object, pods []object) { pods[0]["status"].(object)["containerStatuses"].([]any)[0].(object)["containerID"] = "" }, - "blanket eviction": func(d, p, r object, pods []object) { + "blanket eviction": func(_, _, _ object, pods []object) { pods[0]["metadata"].(object)["annotations"].(object)["cluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes"] = "sigstore,data" }, - "persistent cache": func(d, p, r object, pods []object) { + "persistent cache": func(_, _, _ object, pods []object) { pods[0]["spec"].(object)["volumes"] = []any{object{"name": "sigstore", "persistentVolumeClaim": object{"claimName": "data"}}} }, } { diff --git a/scripts/prove-kyverno-cache-recovery/main.go b/scripts/prove-kyverno-cache-recovery/main.go index f3dfe646e9..4ba69b8279 100644 --- a/scripts/prove-kyverno-cache-recovery/main.go +++ b/scripts/prove-kyverno-cache-recovery/main.go @@ -1,3 +1,5 @@ +// Command prove-kyverno-cache-recovery verifies Kyverno admission after its +// ephemeral cache recovers. package main import ( diff --git a/scripts/prove-kyverno-cache-recovery/service_test.go b/scripts/prove-kyverno-cache-recovery/service_test.go index a1e99d4741..c065ba5e11 100644 --- a/scripts/prove-kyverno-cache-recovery/service_test.go +++ b/scripts/prove-kyverno-cache-recovery/service_test.go @@ -36,17 +36,17 @@ func TestWebhookServiceRoutesOnlyToTheQualifiedProcesses(t *testing.T) { func TestForeignOrUnavailableEndpointCannotPass(t *testing.T) { for name, change := range map[string]func(object, object){ - "wrong service selector": func(s, e object) { s["spec"].(object)["selector"] = object{"foreign": "true"} }, - "external service target": func(s, e object) { + "wrong service selector": func(s, _ object) { s["spec"].(object)["selector"] = object{"foreign": "true"} }, + "external service target": func(s, _ object) { s["spec"].(object)["type"] = "ExternalName" s["spec"].(object)["externalName"] = "foreign.invalid" }, - "wrong service port": func(s, e object) { s["spec"].(object)["ports"].([]any)[0].(object)["targetPort"] = "foreign" }, - "foreign slice owner": func(s, e object) { e["metadata"].(object)["ownerReferences"] = owned("Service", "foreign") }, - "foreign Pod UID": func(s, e object) { e["endpoints"].([]any)[0].(object)["targetRef"].(object)["uid"] = "foreign" }, - "foreign address": func(s, e object) { e["endpoints"].([]any)[0].(object)["addresses"] = []any{"10.0.0.99"} }, - "unready endpoint": func(s, e object) { e["endpoints"].([]any)[0].(object)["conditions"].(object)["ready"] = false }, - "wrong endpoint port": func(s, e object) { e["ports"].([]any)[0].(object)["port"] = 9444 }, + "wrong service port": func(s, _ object) { s["spec"].(object)["ports"].([]any)[0].(object)["targetPort"] = "foreign" }, + "foreign slice owner": func(_, e object) { e["metadata"].(object)["ownerReferences"] = owned("Service", "foreign") }, + "foreign Pod UID": func(_, e object) { e["endpoints"].([]any)[0].(object)["targetRef"].(object)["uid"] = "foreign" }, + "foreign address": func(_, e object) { e["endpoints"].([]any)[0].(object)["addresses"] = []any{"10.0.0.99"} }, + "unready endpoint": func(_, e object) { e["endpoints"].([]any)[0].(object)["conditions"].(object)["ready"] = false }, + "wrong endpoint port": func(_, e object) { e["ports"].([]any)[0].(object)["port"] = 9444 }, } { t.Run(name, func(t *testing.T) { s, e, bindings := serviceFixture(t) diff --git a/scripts/prove-longhorn-ui-readonly/main.go b/scripts/prove-longhorn-ui-readonly/main.go index 1a70355df2..d19b633816 100644 --- a/scripts/prove-longhorn-ui-readonly/main.go +++ b/scripts/prove-longhorn-ui-readonly/main.go @@ -1,3 +1,5 @@ +// Command prove-longhorn-ui-readonly sends fixed, harmless requests through +// the authentication proxy to prove the Longhorn UI is read only. package main import ( diff --git a/scripts/prove-scoped-package-access/main.go b/scripts/prove-scoped-package-access/main.go index a8b9d81750..6fcb25a771 100644 --- a/scripts/prove-scoped-package-access/main.go +++ b/scripts/prove-scoped-package-access/main.go @@ -1,3 +1,5 @@ +// Command prove-scoped-package-access runs the repository-scoped GHCR access +// experiment against the existing encrypted pull credential as a baseline. package main import ( diff --git a/scripts/prove-scoped-package-access/main_test.go b/scripts/prove-scoped-package-access/main_test.go index 6a79cdb154..fc583fd317 100644 --- a/scripts/prove-scoped-package-access/main_test.go +++ b/scripts/prove-scoped-package-access/main_test.go @@ -79,7 +79,7 @@ func TestProofRequiresBothDirectionsAndIndependentControls(t *testing.T) { var redirected, crossed bool const manifest = `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":2},"layers":[]}` digest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(manifest))) - sink := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sink := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { redirected = true w.WriteHeader(http.StatusOK) })) @@ -235,7 +235,7 @@ func TestProofRequiresBothDirectionsAndIndependentControls(t *testing.T) { var pulls []string p := proof{apiURL: server.URL, registryURL: server.URL, client: restrictedClient(), output: &output, baseline: credential{"baseline-user", "baseline-secret"}, scoped: credential{"x-access-token", "scoped-secret"}, - pull: func(ctx context.Context, auth credential, image string) error { + pull: func(_ context.Context, auth credential, image string) error { if !strings.HasSuffix(image, "@"+digest) { t.Errorf("pull is not bound to measured digest: %s", image) } diff --git a/scripts/reconcile-talos-kernel-args/main_test.go b/scripts/reconcile-talos-kernel-args/main_test.go index c1b8a951ba..cb11983b99 100644 --- a/scripts/reconcile-talos-kernel-args/main_test.go +++ b/scripts/reconcile-talos-kernel-args/main_test.go @@ -870,12 +870,12 @@ func TestFetchIsRetriedBeforeItFails(t *testing.T) { dir, _ := sourceRepository(t) bin := t.TempDir() count := filepath.Join(bin, "fetches") - real, err := exec.LookPath("git") + realGit, err := exec.LookPath("git") if err != nil { t.Fatal(err) } // Fail the first fetch, then hand over to git. - script := "#!/bin/sh\nfor a in \"$@\"; do if [ \"$a\" = fetch ]; then echo x >> '" + count + "'; if [ \"$(wc -l < '" + count + "')\" -lt 2 ]; then echo transient >&2; exit 128; fi; fi; done\nexec '" + real + "' \"$@\"\n" + script := "#!/bin/sh\nfor a in \"$@\"; do if [ \"$a\" = fetch ]; then echo x >> '" + count + "'; if [ \"$(wc -l < '" + count + "')\" -lt 2 ]; then echo transient >&2; exit 128; fi; fi; done\nexec '" + realGit + "' \"$@\"\n" if err := os.WriteFile(filepath.Join(bin, "git"), []byte(script), 0o700); err != nil { t.Fatal(err) } diff --git a/scripts/recover-private-mail-probes/main.go b/scripts/recover-private-mail-probes/main.go index 8fa15a16e8..e2161add6d 100644 --- a/scripts/recover-private-mail-probes/main.go +++ b/scripts/recover-private-mail-probes/main.go @@ -1,3 +1,5 @@ +// Command recover-private-mail-probes performs the fixed recovery of the +// obsolete private-mail probe Pod and verifies its retained claim. package main import ( @@ -435,7 +437,7 @@ type boundedOutput struct{ bytes.Buffer } func (b *boundedOutput) Write(p []byte) (int, error) { if b.Len()+len(p) > 1<<20 { - return 0, errors.New("Kubernetes response too large") + return 0, errors.New("oversized Kubernetes response") } return b.Buffer.Write(p) } @@ -462,7 +464,7 @@ func (c commandClient) run(ctx context.Context, input []byte, args ...string) ([ started := time.Now() if err := cmd.Run(); err != nil { if c.diagnostics != nil { - fmt.Fprintf(c.diagnostics, "kubectl %s: %v after %s, deadline: %v\n", args[0], err, time.Since(started).Round(time.Millisecond), requestCtx.Err()) + _, _ = fmt.Fprintf(c.diagnostics, "kubectl %s: %v after %s, deadline: %v\n", args[0], err, time.Since(started).Round(time.Millisecond), requestCtx.Err()) } return nil, errors.New("bounded Kubernetes request failed") } diff --git a/scripts/replace-wedding-standby/cluster_transport_test.go b/scripts/replace-wedding-standby/cluster_transport_test.go index 8e5a8ad2ff..64e75ba3eb 100644 --- a/scripts/replace-wedding-standby/cluster_transport_test.go +++ b/scripts/replace-wedding-standby/cluster_transport_test.go @@ -402,7 +402,7 @@ func TestClusterTransportRefusesUnverifiableResponse(t *testing.T) { } // TestKubeconfigExportHelper is a subprocess fixture, never a real kubectl call. -func TestKubeconfigExportHelper(t *testing.T) { +func TestKubeconfigExportHelper(_ *testing.T) { switch os.Getenv("WEDDING_CONFIG_TEST_HELPER") { case "stdout": _, _ = io.WriteString(os.Stdout, strings.Repeat("x", transportByteLimit+1)) diff --git a/scripts/replace-wedding-standby/quarantine_test.go b/scripts/replace-wedding-standby/quarantine_test.go index f4357ffb14..a85cf5b2f6 100644 --- a/scripts/replace-wedding-standby/quarantine_test.go +++ b/scripts/replace-wedding-standby/quarantine_test.go @@ -423,9 +423,10 @@ func testCompletedJoinTransaction(t *testing.T, continuation bool) { at(lease, "spec")["renewTime"] = now.Add(-time.Second).Format(time.RFC3339Nano) if strings.HasPrefix(failure, "refresh marker ") { changeGuard() - if failure == "refresh marker owner" { + switch failure { + case "refresh marker owner": list(s.cluster, "metadata", "managedFields")[0]["manager"] = "other" - } else if failure == "refresh marker marker" { + case "refresh marker marker": at(s.cluster, "metadata", "annotations")[pauseObservationKey] = "other" } } @@ -451,9 +452,10 @@ func testCompletedJoinTransaction(t *testing.T, continuation bool) { at(lease, "spec")["renewTime"] = now.Add(-time.Second).Format(time.RFC3339Nano) } if continuation && len(writes) == 1 { - if failure == "changed observation" { + switch failure { + case "changed observation": at(s.cluster, "metadata", "annotations")[pauseObservationKey] = "other" - } else if failure == "foreign observation owner" { + case "foreign observation owner": at(s.cluster, "metadata")["managedFields"] = append(value(s.cluster, "metadata", "managedFields").([]any), object{"manager": "other", "fieldsV1": object{"f:metadata": object{"f:annotations": object{"f:" + pauseObservationKey: object{}}}}}) } } diff --git a/scripts/retire-arc-pool/transaction.go b/scripts/retire-arc-pool/transaction.go index acc97cc5f3..c67d970b12 100644 --- a/scripts/retire-arc-pool/transaction.go +++ b/scripts/retire-arc-pool/transaction.go @@ -171,7 +171,7 @@ func readJournal(s state) (journal, error) { if json.Unmarshal(body, &canonical) != nil { return j, errors.New("invalid retirement journal") } - if len(canonical) != 9 && !(len(canonical) == 10 && j.Baseline != nil) { + if len(canonical) != 9 && (len(canonical) != 10 || j.Baseline == nil) { return j, errors.New("incomplete retirement journal") } for _, key := range []string{"namespaceUID", "hrUID", "esoUID", "phase", "controllerUID", "controllerTicket"} { diff --git a/scripts/retire-arc-pool/writer_test.go b/scripts/retire-arc-pool/writer_test.go index 05f7e14248..24ca0bf312 100644 --- a/scripts/retire-arc-pool/writer_test.go +++ b/scripts/retire-arc-pool/writer_test.go @@ -49,7 +49,9 @@ func TestWriterBarrierSurvivesClosedSourceReadinessButNotChangedAuthority(t *tes } for _, kind := range []string{"source-uid", "source-generation", "source-revision", "invalid-archive-checksum", "source-unverified", "layer-uid", "layer-generation", "disabled-layer", "producer-alias"} { var p map[string]any - json.Unmarshal(body, &p) + if err := json.Unmarshal(body, &p); err != nil { + t.Fatal(err) + } src := p["OCI"].(map[string]any) layers := p["Kustomizations"].([]any) layer := layers[0].(map[string]any) @@ -94,7 +96,9 @@ func TestWriterReceiptRequiresCompleteFreshBarrierBeforeDrain(t *testing.T) { } raw := sourceFixture(t) var proof map[string]any - json.Unmarshal(raw, &proof) + if err := json.Unmarshal(raw, &proof); err != nil { + t.Fatal(err) + } proof["Ticket"] = "native-123-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" for _, layer := range proof["Kustomizations"].([]any) { layer.(map[string]any)["metadata"].(map[string]any)["annotations"] = map[string]any{"reconcile.fluxcd.io/requestedAt": proof["Ticket"]} @@ -137,7 +141,9 @@ func TestOpeningWriterBarrierRequiresTheCompletedExactMissingCreateDenial(t *tes applyRecord(t, &s, p) j, _ := readJournal(s) var proof map[string]any - json.Unmarshal(sourceFixture(t), &proof) + if err := json.Unmarshal(sourceFixture(t), &proof); err != nil { + t.Fatal(err) + } proof["Digest"] = j.Baseline.Digest proof["Ticket"] = "native-123-1-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" proof["FluxBefore"], proof["Flux"] = fluxFixture(), fluxFixture() @@ -171,7 +177,9 @@ func TestOpeningWriterBarrierRequiresTheCompletedExactMissingCreateDenial(t *tes } for _, kind := range []string{"generic-error", "wrong-policy", "wrong-target", "active-apply", "stale-condition", "unacknowledged", "different-digest", "other-layer-failed", "runtime-version", "runtime-rollout", "runtime-old-pod", "runtime-replaced", "duplicate-condition", "stalled", "bound-missing-identity"} { var mutated map[string]any - json.Unmarshal(body, &mutated) + if err := json.Unmarshal(body, &mutated); err != nil { + t.Fatal(err) + } layers := mutated["Kustomizations"].([]any) layer := layers[2].(map[string]any) status := layer["status"].(map[string]any) @@ -228,7 +236,9 @@ func TestOpeningWriterBarrierRequiresTheCompletedExactMissingCreateDenial(t *tes func TestCompletedFailureReceiptRejectsChangedRuntimeAtDelete(t *testing.T) { var proof map[string]any - json.Unmarshal(sourceFixture(t), &proof) + if err := json.Unmarshal(sourceFixture(t), &proof); err != nil { + t.Fatal(err) + } flux := fluxFixture() proof["Flux"] = flux raw, _ := json.Marshal(flux) diff --git a/scripts/tests/contract-probe-image/native_test.go b/scripts/tests/contract-probe-image/native_test.go index 91ed036fe6..19511aba21 100644 --- a/scripts/tests/contract-probe-image/native_test.go +++ b/scripts/tests/contract-probe-image/native_test.go @@ -27,7 +27,7 @@ func TestPublishedProbeImage(t *testing.T) { defer cancel() declaration := filepath.Join("..", "..", "..", "k8s", "bases", "apps", "data-product-controller", "deployment-contract-probe.yaml") - data := runNative(t, ctx, "read declared image", "yq", "-o=json", ".spec.template.spec.containers", declaration) + data := runNative(ctx, t, "read declared image", "yq", "-o=json", ".spec.template.spec.containers", declaration) var containers []struct { Name string `json:"name"` Image string `json:"image"` @@ -51,7 +51,7 @@ func TestPublishedProbeImage(t *testing.T) { if err := os.Chmod(client, 0o555); err != nil { t.Fatal("could not make the isolated client executable") } - runNative(t, ctx, "pull declared image", "docker", "pull", "--platform", "linux/amd64", image) + runNative(ctx, t, "pull declared image", "docker", "pull", "--platform", "linux/amd64", image) name := fmt.Sprintf("contract-probe-proof-%d-%d", os.Getpid(), time.Now().UnixNano()) // Register before create: a partially completed Docker operation may still // leave this unique, test-owned container behind. @@ -63,14 +63,14 @@ func TestPublishedProbeImage(t *testing.T) { t.Errorf("native probe cleanup was not confirmed: %v\n%s", err, output) } }) - runNative(t, ctx, "create isolated probe", "docker", "create", "--name", name, + runNative(ctx, t, "create isolated probe", "docker", "create", "--name", name, "--platform", "linux/amd64", "--network", "none", "--read-only", "--user", "65532:65532", "--cpus", "1", "--memory", "64m", "--cap-drop", "ALL", "--security-opt", "no-new-privileges:true", "--mount", "type=bind,source="+client+",target=/management-proof.test,readonly", "--env", "CONTRACT_PROBE_URL=https://contract-probe.invalid/openapi.json", "--env", "CONTRACT_READINESS_ENABLED=true", "--entrypoint", "/contract-probe", image) - runNative(t, ctx, "start isolated probe", "docker", "start", name) - proof := runNative(t, ctx, "exercise published diagnostics", "docker", "exec", + runNative(ctx, t, "start isolated probe", "docker", "start", name) + proof := runNative(ctx, t, "exercise published diagnostics", "docker", "exec", "--env", "CONTRACT_PROBE_IMAGE_INSIDE=1", name, "/management-proof.test", "-test.run", "^TestInsidePublishedProbe$", "-test.v", "-test.timeout", "30s") if !strings.Contains(string(proof), "--- PASS: TestInsidePublishedProbe") { @@ -81,7 +81,7 @@ func TestPublishedProbeImage(t *testing.T) { // runNative keeps failures attributable to the fixed fixture stage. No platform // credentials or workload response data are supplied to these commands. -func runNative(t *testing.T, ctx context.Context, stage, program string, args ...string) []byte { +func runNative(ctx context.Context, t *testing.T, stage, program string, args ...string) []byte { t.Helper() output, err := exec.CommandContext(ctx, program, args...).CombinedOutput() if err != nil { diff --git a/scripts/tests/coroot-security-dashboards/reconcile_test.go b/scripts/tests/coroot-security-dashboards/reconcile_test.go index ec32b8786e..4e7aad1aa3 100644 --- a/scripts/tests/coroot-security-dashboards/reconcile_test.go +++ b/scripts/tests/coroot-security-dashboards/reconcile_test.go @@ -359,7 +359,7 @@ func TestInputMustBeOwnedAndUnambiguous(t *testing.T) { func TestRequestTimeoutIsBounded(t *testing.T) { c := newCoroot() - c.override = func(w http.ResponseWriter, r *http.Request) bool { + c.override = func(_ http.ResponseWriter, r *http.Request) bool { <-r.Context().Done() return true } diff --git a/scripts/tests/fixtures/coroot-dns-probe/main.go b/scripts/tests/fixtures/coroot-dns-probe/main.go index 0fcffa8cda..b3aedcade0 100644 --- a/scripts/tests/fixtures/coroot-dns-probe/main.go +++ b/scripts/tests/fixtures/coroot-dns-probe/main.go @@ -25,7 +25,7 @@ func run() error { if err != nil { return err } - defer server.Close() + defer func() { _ = server.Close() }() var mu sync.Mutex var queries []string go func() { diff --git a/scripts/tests/openbao-transport-runtime/tls_test.go b/scripts/tests/openbao-transport-runtime/tls_test.go index eef58c2e8d..9c97605cca 100644 --- a/scripts/tests/openbao-transport-runtime/tls_test.go +++ b/scripts/tests/openbao-transport-runtime/tls_test.go @@ -135,9 +135,9 @@ func TestNativeTLSAndCertificateReload(t *testing.T) { } }) endpoint := fmt.Sprintf("https://127.0.0.1:%d/v1/sys/health", tlsPort) - client := func(servername string, roots *x509.CertPool, min, max uint16) *http.Client { + client := func(servername string, roots *x509.CertPool, minVersion, maxVersion uint16) *http.Client { return &http.Client{Timeout: time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{ - RootCAs: roots, ServerName: servername, MinVersion: min, MaxVersion: max, + RootCAs: roots, ServerName: servername, MinVersion: minVersion, MaxVersion: maxVersion, }, DisableKeepAlives: true}, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} } readHealth := func(c *http.Client) (int64, error) { @@ -145,7 +145,7 @@ func TestNativeTLSAndCertificateReload(t *testing.T) { if err != nil { return 0, err } - defer response.Body.Close() + defer func() { _ = response.Body.Close() }() _, _ = io.Copy(io.Discard, response.Body) if response.StatusCode != 501 || response.TLS == nil || len(response.TLS.PeerCertificates) == 0 { return 0, fmt.Errorf("unexpected native empty-store health response") diff --git a/scripts/tests/refresh-flux-ghcr-auth/historical_node_phase_test.go b/scripts/tests/refresh-flux-ghcr-auth/historical_node_phase_test.go index 7f959a4d64..41dd844967 100644 --- a/scripts/tests/refresh-flux-ghcr-auth/historical_node_phase_test.go +++ b/scripts/tests/refresh-flux-ghcr-auth/historical_node_phase_test.go @@ -17,24 +17,24 @@ func TestHistoricalNodePhasePlanRequiresCurrentRuntimeProof(t *testing.T) { noPatch bool }{ {name: "mutating", want: true}, - {name: "claimed", want: true, change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-phase"] = "claimed" }}, - {name: "absent", want: true, noPatch: true, change: func(n, a map[string]any) { delete(a, "platform.devantler.tech/ghcr-auth-drain-phase") }}, - {name: "unknown phase", change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-phase"] = "unknown" }}, - {name: "empty phase", change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-phase"] = "" }}, - {name: "active owner", change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-owner"] = "other" }}, - {name: "recovery", change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-recovery"] = "journal" }}, - {name: "scale guard owner", change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-auth-scale-down-owner"] = "other" }}, - {name: "operator cordon", change: func(n, a map[string]any) { n["spec"].(map[string]any)["unschedulable"] = true }}, - {name: "missing revision", change: func(n, a map[string]any) { delete(a, "platform.devantler.tech/ghcr-pull-verified-revision-v2") }}, - {name: "stale revision", change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-pull-verified-revision-v2"] = "old" }}, - {name: "stale image", change: func(n, a map[string]any) { a["platform.devantler.tech/ghcr-pull-verified-image-v2"] = "old" }}, - {name: "changed identity", change: func(n, a map[string]any) { n["metadata"].(map[string]any)["uid"] = "replacement" }}, - {name: "missing identity", change: func(n, a map[string]any) { delete(n["metadata"].(map[string]any), "uid") }}, - {name: "missing resource version", change: func(n, a map[string]any) { delete(n["metadata"].(map[string]any), "resourceVersion") }}, - {name: "deleting", change: func(n, a map[string]any) { + {name: "claimed", want: true, change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-phase"] = "claimed" }}, + {name: "absent", want: true, noPatch: true, change: func(_, a map[string]any) { delete(a, "platform.devantler.tech/ghcr-auth-drain-phase") }}, + {name: "unknown phase", change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-phase"] = "unknown" }}, + {name: "empty phase", change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-phase"] = "" }}, + {name: "active owner", change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-owner"] = "other" }}, + {name: "recovery", change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-auth-drain-recovery"] = "journal" }}, + {name: "scale guard owner", change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-auth-scale-down-owner"] = "other" }}, + {name: "operator cordon", change: func(n, _ map[string]any) { n["spec"].(map[string]any)["unschedulable"] = true }}, + {name: "missing revision", change: func(_, a map[string]any) { delete(a, "platform.devantler.tech/ghcr-pull-verified-revision-v2") }}, + {name: "stale revision", change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-pull-verified-revision-v2"] = "old" }}, + {name: "stale image", change: func(_, a map[string]any) { a["platform.devantler.tech/ghcr-pull-verified-image-v2"] = "old" }}, + {name: "changed identity", change: func(n, _ map[string]any) { n["metadata"].(map[string]any)["uid"] = "replacement" }}, + {name: "missing identity", change: func(n, _ map[string]any) { delete(n["metadata"].(map[string]any), "uid") }}, + {name: "missing resource version", change: func(n, _ map[string]any) { delete(n["metadata"].(map[string]any), "resourceVersion") }}, + {name: "deleting", change: func(n, _ map[string]any) { n["metadata"].(map[string]any)["deletionTimestamp"] = "2026-10-04T00:00:00Z" }}, - {name: "unready", change: func(n, a map[string]any) { + {name: "unready", change: func(n, _ map[string]any) { n["status"].(map[string]any)["conditions"] = []any{map[string]any{"type": "Ready", "status": "False"}} }}, } { diff --git a/scripts/tests/security-progress-metrics/project_scope_test.go b/scripts/tests/security-progress-metrics/project_scope_test.go index 162160d5d1..df2e7bae3f 100644 --- a/scripts/tests/security-progress-metrics/project_scope_test.go +++ b/scripts/tests/security-progress-metrics/project_scope_test.go @@ -104,7 +104,7 @@ func TestProjectBindingRejectsTruncatedHTTPWithValidJSONBody(t *testing.T) { t.Error(err) return } - defer conn.Close() + defer func() { _ = conn.Close() }() _, _ = fmt.Fprint(buffer, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 4096\r\n\r\n"+ `{"projects":[{"id":"synthetic-platform","name":"platform"}]}`) _ = buffer.Flush() diff --git a/scripts/tests/webapp-compiler/tenant_ownership_test.go b/scripts/tests/webapp-compiler/tenant_ownership_test.go index 5705d3220a..51d455015d 100644 --- a/scripts/tests/webapp-compiler/tenant_ownership_test.go +++ b/scripts/tests/webapp-compiler/tenant_ownership_test.go @@ -45,7 +45,7 @@ func tenantControllerMetadataError(objects map[string]*unstructured.Unstructured // Exercise the pinned SSA implementation against the isolated real API server. // The competing writer uses the actual Kyverno generate body, not its controller. -func assertTenantNetworkFloor(t *testing.T, ctx context.Context, config *rest.Config, root string, objects map[string]*unstructured.Unstructured) { +func assertTenantNetworkFloor(ctx context.Context, t *testing.T, config *rest.Config, root string, objects map[string]*unstructured.Unstructured) { t.Helper() t.Run("network-floor-settles-with-competing-generator", func(t *testing.T) { clients, err := kroclient.NewSet(kroclient.Config{RestConfig: rest.CopyConfig(config)}) diff --git a/scripts/tests/webapp-compiler/tenant_test.go b/scripts/tests/webapp-compiler/tenant_test.go index bb7ba42f1d..339e0cca43 100644 --- a/scripts/tests/webapp-compiler/tenant_test.go +++ b/scripts/tests/webapp-compiler/tenant_test.go @@ -189,7 +189,7 @@ func TestCommittedTenant(t *testing.T) { assertTenantChildren(t, objects, ignored, false, sops) if !sops { t.Run("controller-ownership", func(t *testing.T) { - assertTenantNetworkFloor(t, ctx, config, root, objects) + assertTenantNetworkFloor(ctx, t, config, root, objects) }) } source := objects["ociRepository"] diff --git a/scripts/validate-break-glass/main.go b/scripts/validate-break-glass/main.go index 3fc2b7eb86..d11d60c2dd 100644 --- a/scripts/validate-break-glass/main.go +++ b/scripts/validate-break-glass/main.go @@ -1,3 +1,5 @@ +// Command validate-break-glass validates the named recovery fields of a KV v2 +// JSON export read from standard input. package main import ( diff --git a/scripts/validate-dr-signing/main.go b/scripts/validate-dr-signing/main.go index 8f7dcf27d3..c74073e364 100644 --- a/scripts/validate-dr-signing/main.go +++ b/scripts/validate-dr-signing/main.go @@ -225,7 +225,7 @@ var drPublisherCredentials = []drPublisherCredential{ // // 🔴 THE WORKFLOW IS PARSED, NOT SCANNED — the third route, and the last one to // be converted. The two production routes were parsed in #2939; this one still -// asked `containsLine`, which is satisfied by the expected text appearing +// asked a since-removed line scan, which is satisfied by the expected text appearing // ANYWHERE in the job's text. Five ways that said "wired" about a DR route that // was not, each reproduced as an ablation and each returning nil against the // scanning version: @@ -1649,14 +1649,6 @@ func stringListContains(value any, want string) bool { return false } -func containsLine(block string, want string) bool { - for _, line := range strings.Split(block, "\n") { - if strings.Contains(line, want) { - return true - } - } - return false -} func run(workflowPath string, configPath string, stdout io.Writer, stderr io.Writer) int { workflow, err := os.ReadFile(workflowPath) //nolint:gosec // The explicit CLI path is the validator input. if err != nil { diff --git a/scripts/validate-feature-flags/main.go b/scripts/validate-feature-flags/main.go index 6948c77d1a..68e6ae36a4 100644 --- a/scripts/validate-feature-flags/main.go +++ b/scripts/validate-feature-flags/main.go @@ -1,3 +1,5 @@ +// Command validate-feature-flags checks every authored FeatureFlag definition +// against the pinned flagd schema. package main import ( diff --git a/scripts/validate-flux-force-safety/main.go b/scripts/validate-flux-force-safety/main.go index a898a34892..b04f7b017d 100644 --- a/scripts/validate-flux-force-safety/main.go +++ b/scripts/validate-flux-force-safety/main.go @@ -1,3 +1,5 @@ +// Command validate-flux-force-safety verifies that Flux layers and tenant +// templates cannot force replacement and persistent resources cannot opt in. package main import ( diff --git a/scripts/validate-homepage-bookmarks/main.go b/scripts/validate-homepage-bookmarks/main.go index f857515aeb..d753359063 100644 --- a/scripts/validate-homepage-bookmarks/main.go +++ b/scripts/validate-homepage-bookmarks/main.go @@ -33,6 +33,8 @@ var configFiles = []string{ func main() { os.Exit(run(os.Args[1:], os.Stdout)) } func run(args []string, out io.Writer) (status int) { + // bufio.Writer keeps its first write error and Flush returns it, so the + // writes below discard their own results and the deferred Flush decides. buffer := bufio.NewWriter(out) out = buffer defer func() { @@ -41,27 +43,27 @@ func run(args []string, out io.Writer) (status int) { } }() if len(args) != 2 { - fmt.Fprintln(out, "::error::usage: validate-homepage-bookmarks ") + _, _ = fmt.Fprintln(out, "::error::usage: validate-homepage-bookmarks ") return 2 } data, err := os.ReadFile(args[0]) if err != nil { - fmt.Fprintf(out, "::error::missing or unreadable: %s\n", args[0]) + _, _ = fmt.Fprintf(out, "::error::missing or unreadable: %s\n", args[0]) return 2 } info, err := os.Stat(args[1]) if err != nil || !info.IsDir() { - fmt.Fprintf(out, "::error::missing k8s root: %s\n", args[1]) + _, _ = fmt.Fprintf(out, "::error::missing k8s root: %s\n", args[1]) return 2 } var manifest map[string]any if err := decode(data, &manifest); err != nil { - fmt.Fprintf(out, "::error::cannot parse %s: %s\n", args[0], err) + _, _ = fmt.Fprintf(out, "::error::cannot parse %s: %s\n", args[0], err) return 2 } values, ok := manifest["data"].(map[string]any) if manifest == nil || !ok { - fmt.Fprintln(out, "::error::manifest and data must be YAML mappings") + _, _ = fmt.Fprintln(out, "::error::manifest and data must be YAML mappings") return 2 } var missing []string @@ -75,25 +77,25 @@ func run(args []string, out io.Writer) (status int) { if strings.HasSuffix(key, ".yaml") && strings.TrimSpace(body) != "" { var value any if err := decode([]byte(body), &value); err != nil { - fmt.Fprintf(out, "::error::cannot parse %s: %s\n", key, err) + _, _ = fmt.Fprintf(out, "::error::cannot parse %s: %s\n", key, err) return 2 } } } if len(missing) > 0 { - fmt.Fprintf(out, "::error::%s missing from %s; Homepage exits without every config file\n", strings.Join(missing, ", "), args[0]) + _, _ = fmt.Fprintf(out, "::error::%s missing from %s; Homepage exits without every config file\n", strings.Join(missing, ", "), args[0]) return 1 } parsed := make(map[string]any) for _, key := range []string{"bookmarks.yaml", "settings.yaml", "services.yaml"} { body, ok := values[key].(string) if !ok || strings.TrimSpace(body) == "" { - fmt.Fprintf(out, "::error::%s missing from %s\n", key, args[0]) + _, _ = fmt.Fprintf(out, "::error::%s missing from %s\n", key, args[0]) return 1 } var value any if err := decode([]byte(body), &value); err != nil { - fmt.Fprintf(out, "::error::cannot parse %s: %s\n", key, err) + _, _ = fmt.Fprintf(out, "::error::cannot parse %s: %s\n", key, err) return 2 } parsed[key] = value @@ -101,18 +103,18 @@ func run(args []string, out io.Writer) (status int) { services := make(map[string]bool) serviceGroups, ok := parsed["services.yaml"].([]any) if !ok { - fmt.Fprintln(out, "::error::unsupported service group structure") + _, _ = fmt.Fprintln(out, "::error::unsupported service group structure") return 2 } for _, group := range serviceGroups { object, ok := group.(map[string]any) if !ok || len(object) == 0 { - fmt.Fprintln(out, "::error::unsupported service group structure") + _, _ = fmt.Fprintln(out, "::error::unsupported service group structure") return 2 } for name, value := range object { if _, ok := value.([]any); !ok || strings.TrimSpace(name) == "" { - fmt.Fprintln(out, "::error::unsupported service group structure") + _, _ = fmt.Fprintln(out, "::error::unsupported service group structure") return 2 } services[name] = true @@ -147,7 +149,7 @@ func run(args []string, out io.Writer) (status int) { return nil }) if err != nil { - fmt.Fprintf(out, "::error::cannot read service groups: %s\n", err) + _, _ = fmt.Fprintf(out, "::error::cannot read service groups: %s\n", err) return 2 } layout := make(map[string]bool) @@ -161,13 +163,13 @@ func run(args []string, out io.Writer) (status int) { problems, entries, groups := validate(parsed["bookmarks.yaml"], services, layout) if len(problems) > 0 { sort.Strings(problems) - fmt.Fprintf(out, "::error::%d homepage bookmark violation(s):\n", len(problems)) + _, _ = fmt.Fprintf(out, "::error::%d homepage bookmark violation(s):\n", len(problems)) for _, problem := range problems { - fmt.Fprintf(out, " %s\n", problem) + _, _ = fmt.Fprintf(out, " %s\n", problem) } return 1 } - fmt.Fprintf(out, "✓ %d homepage bookmark(s) in %d group(s) valid.\n", entries, groups) + _, _ = fmt.Fprintf(out, "✓ %d homepage bookmark(s) in %d group(s) valid.\n", entries, groups) return 0 } diff --git a/scripts/validate-image-verification-policy/main.go b/scripts/validate-image-verification-policy/main.go index 9c8c1bf2f4..a5a4c71f4a 100644 --- a/scripts/validate-image-verification-policy/main.go +++ b/scripts/validate-image-verification-policy/main.go @@ -131,17 +131,17 @@ func parseInstalled(input io.Reader) ([]rule, error) { func compare(policy string, input io.Reader, _ io.Writer, diagnostic io.Writer) int { expected, err := parsePolicy([]byte(policy)) if err != nil { - fmt.Fprintln(diagnostic, "could not parse declared verification policy") + _, _ = fmt.Fprintln(diagnostic, "could not parse declared verification policy") return 2 } actual, err := parseInstalled(input) if err != nil { - fmt.Fprintln(diagnostic, "could not parse complete installed verification decisions") + _, _ = fmt.Fprintln(diagnostic, "could not parse complete installed verification decisions") return 2 } if !reflect.DeepEqual(expected, actual) { // Do not include actual node specs, issuers, subjects or certificates. - fmt.Fprintln(diagnostic, "installed verification decisions differ from declared policy") + _, _ = fmt.Fprintln(diagnostic, "installed verification decisions differ from declared policy") return 1 } return 0 @@ -153,7 +153,7 @@ func compare(policy string, input io.Reader, _ io.Writer, diagnostic io.Writer) // use the node's actual decisions rather than that lossy intermediate form. func observeResources(kind string, input io.Reader, output, diagnostic io.Writer) int { if kind != "imageverificationrules.security.talos.dev" && kind != "tuftrustedroots.security.talos.dev" { - fmt.Fprintln(diagnostic, "unsupported resource observation") + _, _ = fmt.Fprintln(diagnostic, "unsupported resource observation") return 2 } type observation struct { @@ -180,7 +180,7 @@ func observeResources(kind string, input io.Reader, output, diagnostic io.Writer break } if err != nil || resource.Metadata.ID == "" || seen[resource.Metadata.ID] { - fmt.Fprintln(diagnostic, "invalid or duplicate resource identity") + _, _ = fmt.Fprintln(diagnostic, "invalid or duplicate resource identity") return 2 } seen[resource.Metadata.ID] = true @@ -193,7 +193,7 @@ func observeResources(kind string, input io.Reader, output, diagnostic io.Writer } encoded, err := json.Marshal(observations) if err != nil { - fmt.Fprintln(diagnostic, "could not encode complete resource observations") + _, _ = fmt.Fprintln(diagnostic, "could not encode complete resource observations") return 2 } if _, err := output.Write(append(encoded, '\n')); err != nil { @@ -207,12 +207,12 @@ func run(args []string, input io.Reader, output, diagnostic io.Writer) int { return observeResources(args[1], input, output, diagnostic) } if len(args) != 2 || (args[0] != "normalize" && args[0] != "compare") { - fmt.Fprintln(diagnostic, "usage: validate-image-verification-policy | resources ") + _, _ = fmt.Fprintln(diagnostic, "usage: validate-image-verification-policy | resources ") return 2 } data, err := os.ReadFile(args[1]) if err != nil { - fmt.Fprintln(diagnostic, "could not read declared verification policy") + _, _ = fmt.Fprintln(diagnostic, "could not read declared verification policy") return 2 } if args[0] == "compare" { @@ -220,7 +220,7 @@ func run(args []string, input io.Reader, output, diagnostic io.Writer) int { } rules, err := parsePolicy(data) if err != nil { - fmt.Fprintln(diagnostic, "could not parse declared verification policy") + _, _ = fmt.Fprintln(diagnostic, "could not parse declared verification policy") return 2 } if err := json.NewEncoder(output).Encode(rules); err != nil { diff --git a/scripts/validate-kubescape-frameworks/shell.go b/scripts/validate-kubescape-frameworks/shell.go index 3fee1333db..081110d735 100644 --- a/scripts/validate-kubescape-frameworks/shell.go +++ b/scripts/validate-kubescape-frameworks/shell.go @@ -686,7 +686,7 @@ func maskActionsShellExpressions(source string) (string, error) { } } if j >= len(source) { - return "", fmt.Errorf("Actions expression envelope is not decidable from the text") + return "", fmt.Errorf("the Actions expression envelope is not decidable from the text") } if strings.TrimSpace(source[i+3:j]) == "" { return "", fmt.Errorf("empty Actions expression cannot identify an executable value") diff --git a/scripts/validate-merge-group-heal/flux_bridge_test.go b/scripts/validate-merge-group-heal/flux_bridge_test.go index 5b7c8730b2..d53c9ff570 100644 --- a/scripts/validate-merge-group-heal/flux_bridge_test.go +++ b/scripts/validate-merge-group-heal/flux_bridge_test.go @@ -183,8 +183,8 @@ func TestFluxBridgeRequiredResult(t *testing.T) { if guard != nil { t.Fatal("duplicate Flux bridge result guard") } - copy := step - guard = © + found := step + guard = &found } if guard == nil || guard.If != "" || guard.Continue != nil || guard.Env["EVENT_NAME"] != "${{ github.event_name }}" || diff --git a/scripts/validate-policy-actions/main.go b/scripts/validate-policy-actions/main.go index a23bf29056..c30608e103 100644 --- a/scripts/validate-policy-actions/main.go +++ b/scripts/validate-policy-actions/main.go @@ -1,3 +1,5 @@ +// Command validate-policy-actions verifies the explicit validation rule +// actions of the cluster policies against the approved baseline. package main import ( diff --git a/scripts/validate-talos-kubernetes-compatibility/main.go b/scripts/validate-talos-kubernetes-compatibility/main.go index 263eab52c3..001acd6da1 100644 --- a/scripts/validate-talos-kubernetes-compatibility/main.go +++ b/scripts/validate-talos-kubernetes-compatibility/main.go @@ -1,3 +1,5 @@ +// Command validate-talos-kubernetes-compatibility checks that the Kubernetes +// and Talos version pins of a KSail cluster config are compatible. package main import ( diff --git a/scripts/validate-talos-kubernetes-compatibility/main_test.go b/scripts/validate-talos-kubernetes-compatibility/main_test.go index 68454efd87..ab085be454 100644 --- a/scripts/validate-talos-kubernetes-compatibility/main_test.go +++ b/scripts/validate-talos-kubernetes-compatibility/main_test.go @@ -201,7 +201,7 @@ func TestHealDeployValidatesCheckedOutMain(t *testing.T) { if deploy < 0 { t.Fatal("heal route does not deploy production") } - if !(checkoutMain < validator && validator < deploy) { + if checkoutMain >= validator || validator >= deploy { t.Errorf("heal route must validate after checkout and before deploy: checkout=%d validator=%d deploy=%d", checkoutMain, validator, deploy) } } diff --git a/scripts/verify-arc-app-identity/config.go b/scripts/verify-arc-app-identity/config.go index e2b4b64933..6b641d5b18 100644 --- a/scripts/verify-arc-app-identity/config.go +++ b/scripts/verify-arc-app-identity/config.go @@ -72,7 +72,7 @@ func readYAMLFile(path string) (map[string]any, error) { if err != nil { return nil, err } - defer file.Close() + defer func() { _ = file.Close() }() data, err := io.ReadAll(io.LimitReader(file, maxResponse+1)) if err != nil || len(data) > maxResponse { return nil, fmt.Errorf("invalid configuration") diff --git a/scripts/verify-arc-app-identity/runner_group.go b/scripts/verify-arc-app-identity/runner_group.go index 088beb704a..2c22fd9abb 100644 --- a/scripts/verify-arc-app-identity/runner_group.go +++ b/scripts/verify-arc-app-identity/runner_group.go @@ -164,7 +164,7 @@ func (api groupAPI) call(ctx context.Context, method, path string, body []byte, if err != nil { return 0, "HOLD_CAPABILITY" } - defer response.Body.Close() + defer func() { _ = response.Body.Close() }() if response.StatusCode != expected { return response.StatusCode, "HOLD_CAPABILITY" } diff --git a/scripts/verify-arc-app-identity/runner_group_test.go b/scripts/verify-arc-app-identity/runner_group_test.go index 0e98cd403e..49a8f03536 100644 --- a/scripts/verify-arc-app-identity/runner_group_test.go +++ b/scripts/verify-arc-app-identity/runner_group_test.go @@ -170,13 +170,13 @@ func (f *groupFixture) handler(t *testing.T) http.HandlerFunc { } // Copy responses so a one-response fault does not silently rewrite remote state. encoded, _ := json.Marshal(value) - var copy map[string]any - _ = json.Unmarshal(encoded, ©) + var clone map[string]any + _ = json.Unmarshal(encoded, &clone) if f.mutate != nil { - f.mutate(path, copy) + f.mutate(path, clone) } w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(copy) + _ = json.NewEncoder(w).Encode(clone) } } diff --git a/scripts/verify-arc-app-identity/transport_test.go b/scripts/verify-arc-app-identity/transport_test.go index 468e12f383..e053f5ce32 100644 --- a/scripts/verify-arc-app-identity/transport_test.go +++ b/scripts/verify-arc-app-identity/transport_test.go @@ -140,7 +140,7 @@ func TestTransportHealthRejectsTLSBypassesRedirectsAndPartialReads(t *testing.T) t.Error(err) return } - defer connection.Close() + defer func() { _ = connection.Close() }() _, _ = io.WriteString(connection, "HTTP/1.1 200 OK\r\nContent-Length: 1000\r\n\r\n{\"initialized\":true,\"sealed\":false}") return } diff --git a/scripts/verify-arc-app-identity/verify.go b/scripts/verify-arc-app-identity/verify.go index f5ebfe60b3..400717b9a7 100644 --- a/scripts/verify-arc-app-identity/verify.go +++ b/scripts/verify-arc-app-identity/verify.go @@ -243,8 +243,8 @@ func doRequest(ctx context.Context, client *http.Client, method, endpoint string if err != nil { return 0, failTransport } - defer response.Body.Close() - if response.StatusCode != http.StatusOK && !(target == nil && response.StatusCode == http.StatusNoContent) { + defer func() { _ = response.Body.Close() }() + if response.StatusCode != http.StatusOK && (target != nil || response.StatusCode != http.StatusNoContent) { return response.StatusCode, failAPI } if target == nil { diff --git a/scripts/verify-email-data-recovery/mailbox_test.go b/scripts/verify-email-data-recovery/mailbox_test.go index 5a853c8055..4d05f90644 100644 --- a/scripts/verify-email-data-recovery/mailbox_test.go +++ b/scripts/verify-email-data-recovery/mailbox_test.go @@ -62,7 +62,7 @@ func TestMailboxRejectsForeignMethodAccountAndCallReceipts(t *testing.T) { "multiple": `{"methodResponses":[["Email/get",{"accountId":"company-account"},"proof"],["Email/get",{"accountId":"company-account"},"proof"]]}`, } { t.Run(name, func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, response) })) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, response) })) defer server.Close() client, _ := newMailboxClient(server.URL, "fixture-only") client.account = "company-account" @@ -75,9 +75,9 @@ func TestMailboxRejectsForeignMethodAccountAndCallReceipts(t *testing.T) { func TestMailboxCredentialsNeverFollowRedirectOrErrorText(t *testing.T) { forwarded := false - other := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { forwarded = true })) + other := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { forwarded = true })) defer other.Close() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Location", other.URL) w.WriteHeader(http.StatusTemporaryRedirect) _, _ = io.WriteString(w, "private-mail-body-do-not-log") diff --git a/scripts/verify-email-data-recovery/main.go b/scripts/verify-email-data-recovery/main.go index 21fb87c810..f72a0f885f 100644 --- a/scripts/verify-email-data-recovery/main.go +++ b/scripts/verify-email-data-recovery/main.go @@ -1,3 +1,5 @@ +// Command verify-email-data-recovery proves that Email mailbox data restores +// from a fresh backup into an isolated server, without moving the serving claim. package main import ( diff --git a/scripts/verify-email-data-recovery/native_test.go b/scripts/verify-email-data-recovery/native_test.go index 9f623e4ae2..5613ec06a3 100644 --- a/scripts/verify-email-data-recovery/native_test.go +++ b/scripts/verify-email-data-recovery/native_test.go @@ -67,7 +67,7 @@ func TestNativeRecoveryMarkerLifecycle(t *testing.T) { marker, err := client.createMarker(ctx, nonce) if err != nil { if marker != nil && marker.identity.email != "" { - logNativeMessageShape(t, ctx, client, marker.identity) + logNativeMessageShape(ctx, t, client, marker.identity) } t.Fatalf("native marker import: %s", err) } @@ -82,7 +82,7 @@ func TestNativeRecoveryMarkerLifecycle(t *testing.T) { // Failure diagnostics reveal only comparisons and types from synthetic data. // No message content, identifier or credential is written to CI output. -func logNativeMessageShape(t *testing.T, ctx context.Context, client *mailboxClient, expected messageIdentity) { +func logNativeMessageShape(ctx context.Context, t *testing.T, client *mailboxClient, expected messageIdentity) { t.Helper() result, err := client.call(ctx, "Email/get", object{"ids": []any{expected.email}, "properties": []any{"id", "blobId", "messageId", "mailboxIds", "textBody", "bodyValues"}, "fetchTextBodyValues": true}) if err != nil { diff --git a/scripts/verify-email-data-recovery/observation_lifecycle_test.go b/scripts/verify-email-data-recovery/observation_lifecycle_test.go index f36ef0db01..f478651a6b 100644 --- a/scripts/verify-email-data-recovery/observation_lifecycle_test.go +++ b/scripts/verify-email-data-recovery/observation_lifecycle_test.go @@ -30,7 +30,7 @@ func (w *observationFailingWriter) Write(p []byte) (int, error) { return w.base.Write(p) } -func TestObservationForwarderChild(t *testing.T) { +func TestObservationForwarderChild(_ *testing.T) { port := os.Getenv("EMAIL_OBSERVATION_FORWARD_PORT") if port == "" { return diff --git a/scripts/verify-email-data-recovery/observation_test.go b/scripts/verify-email-data-recovery/observation_test.go index d4d293900b..2a1c497409 100644 --- a/scripts/verify-email-data-recovery/observation_test.go +++ b/scripts/verify-email-data-recovery/observation_test.go @@ -78,7 +78,7 @@ func TestObservationHealthReportsStatusWithoutAuthenticationOrRedirects(t *testi for _, status := range []int{200, 401, 403, 503, 302} { t.Run(http.StatusText(status), func(t *testing.T) { targetReads := 0 - target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { targetReads++ })) + target := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { targetReads++ })) defer target.Close() source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" || r.URL.Path != "/healthz/ready" || r.Header.Get("Authorization") != "" { diff --git a/scripts/verify-email-data-recovery/proof_test.go b/scripts/verify-email-data-recovery/proof_test.go index a6e02e1570..310a28cc8c 100644 --- a/scripts/verify-email-data-recovery/proof_test.go +++ b/scripts/verify-email-data-recovery/proof_test.go @@ -52,21 +52,21 @@ func TestMailboxSnapshotSelectionIgnoresScratchButRequiresUniqueData(t *testing. // of these foreign, stale, incomplete or empty backups as recovery evidence. func TestUnboundOrIncompleteBackupCannotBecomeRestoreInput(t *testing.T) { mutations := map[string]func(object, object){ - "foreign-backup": func(b, p object) { child(b, "metadata")["uid"] = "replacement" }, - "wrong-owner": func(b, p object) { items(child(p, "metadata"), "ownerReferences")[0]["uid"] = "another-backup" }, - "wrong-pod": func(b, p object) { child(child(p, "spec"), "pod")["uid"] = "replacement-pod" }, - "wrong-namespace": func(b, p object) { child(child(p, "spec"), "pod")["namespace"] = "other" }, - "wrong-volume": func(b, p object) { child(p, "spec")["volume"] = "another" }, - "wrong-location": func(b, p object) { child(p, "spec")["backupStorageLocation"] = "foreign" }, - "incomplete": func(b, p object) { child(p, "status")["phase"] = "InProgress" }, - "partial": func(b, p object) { child(child(p, "status"), "progress")["bytesDone"] = float64(3999) }, - "empty": func(b, p object) { + "foreign-backup": func(b, _ object) { child(b, "metadata")["uid"] = "replacement" }, + "wrong-owner": func(_, p object) { items(child(p, "metadata"), "ownerReferences")[0]["uid"] = "another-backup" }, + "wrong-pod": func(_, p object) { child(child(p, "spec"), "pod")["uid"] = "replacement-pod" }, + "wrong-namespace": func(_, p object) { child(child(p, "spec"), "pod")["namespace"] = "other" }, + "wrong-volume": func(_, p object) { child(p, "spec")["volume"] = "another" }, + "wrong-location": func(_, p object) { child(p, "spec")["backupStorageLocation"] = "foreign" }, + "incomplete": func(_, p object) { child(p, "status")["phase"] = "InProgress" }, + "partial": func(_, p object) { child(child(p, "status"), "progress")["bytesDone"] = float64(3999) }, + "empty": func(_, p object) { child(child(p, "status"), "progress")["bytesDone"] = float64(0) child(child(p, "status"), "progress")["totalBytes"] = float64(0) }, - "invalid-snapshot": func(b, p object) { child(p, "status")["snapshotID"] = "../foreign" }, - "stale": func(b, p object) { child(p, "status")["completionTimestamp"] = "2026-10-28T10:00:00Z" }, - "backup-errors": func(b, p object) { child(b, "status")["errors"] = float64(1) }, + "invalid-snapshot": func(_, p object) { child(p, "status")["snapshotID"] = "../foreign" }, + "stale": func(_, p object) { child(p, "status")["completionTimestamp"] = "2026-10-28T10:00:00Z" }, + "backup-errors": func(b, _ object) { child(b, "status")["errors"] = float64(1) }, } for name, mutate := range mutations { t.Run(name, func(t *testing.T) { diff --git a/scripts/verify-email-data-recovery/source_test.go b/scripts/verify-email-data-recovery/source_test.go index 3f07891fad..13ca18a8fc 100644 --- a/scripts/verify-email-data-recovery/source_test.go +++ b/scripts/verify-email-data-recovery/source_test.go @@ -128,16 +128,16 @@ func TestServingSourceIsBoundAcrossPodClaimAndBackend(t *testing.T) { } func TestSourceReplacementHookOrWrongClaimRefusesBackup(t *testing.T) { mutations := map[string]func(object, object, object){ - "claim-ref": func(p, c, v object) { child(child(v, "spec"), "claimRef")["uid"] = "another-claim" }, - "wrong-pod-volume": func(p, c, v object) { + "claim-ref": func(_, _, v object) { child(child(v, "spec"), "claimRef")["uid"] = "another-claim" }, + "wrong-pod-volume": func(p, _, _ object) { child(items(child(p, "spec"), "volumes")[0], "persistentVolumeClaim")["claimName"] = "another-claim" }, - "unready": func(p, c, v object) { items(child(p, "status"), "conditions")[0]["status"] = "False" }, - "wrong-image": func(p, c, v object) { items(child(p, "spec"), "containers")[0]["image"] = "unreviewed:latest" }, - "hook": func(p, c, v object) { + "unready": func(p, _, _ object) { items(child(p, "status"), "conditions")[0]["status"] = "False" }, + "wrong-image": func(p, _, _ object) { items(child(p, "spec"), "containers")[0]["image"] = "unreviewed:latest" }, + "hook": func(p, _, _ object) { child(p, "metadata")["annotations"] = object{"pre.hook.backup.velero.io/command": "unsafe-command"} }, - "deleting": func(p, c, v object) { child(c, "metadata")["deletionTimestamp"] = "2026-10-29T10:00:00Z" }, + "deleting": func(_, c, _ object) { child(c, "metadata")["deletionTimestamp"] = "2026-10-29T10:00:00Z" }, } for name, mutate := range mutations { t.Run(name, func(t *testing.T) { diff --git a/scripts/verify-email-provider/lifecycle_test.go b/scripts/verify-email-provider/lifecycle_test.go index 3b9edaf4a7..dfd9b44cd8 100644 --- a/scripts/verify-email-provider/lifecycle_test.go +++ b/scripts/verify-email-provider/lifecycle_test.go @@ -206,7 +206,8 @@ func (f *providerFixture) call(_ context.Context, input []byte, args ...string) f.t.Fatal("write escaped fixed diagnostic resource") } kind := o["kind"].(string) - if kind == "Pod" { + switch kind { + case "Pod": spec := o["spec"].(map[string]any) if spec["hostNetwork"] == true || spec["automountServiceAccountToken"] != false || spec["hostUsers"] != false || spec["volumes"] != nil || spec["nodeName"] != nil || spec["restartPolicy"] != "Never" { f.t.Fatal("probe gained host, credential, volume, or scheduler bypass access") @@ -222,11 +223,11 @@ func (f *providerFixture) call(_ context.Context, input []byte, args ...string) if strings.Contains(string(input), "MAIL FROM") || strings.Contains(string(input), "RCPT TO") { f.t.Fatal("probe can send mail") } - } else if kind == "CiliumNetworkPolicy" { + case "CiliumNetworkPolicy": if !strings.Contains(string(input), `"matchName":"mx01.mail.icloud.com"`) || strings.Contains(string(input), `"toEntities"`) { f.t.Fatal("probe network access is not confined to fixed SMTP and DNS") } - } else { + default: f.t.Fatal("unexpected resource kind") } if f.fault == "create-lost" { diff --git a/scripts/verify-email-storage-canary/main.go b/scripts/verify-email-storage-canary/main.go index 8a24466ebd..0020c2b7bd 100644 --- a/scripts/verify-email-storage-canary/main.go +++ b/scripts/verify-email-storage-canary/main.go @@ -1,3 +1,5 @@ +// Command verify-email-storage-canary proves the encrypted email storage +// canary's write, remount, wrong-key refusal, recovery and cleanup. package main import ( diff --git a/scripts/verify-email-storage-canary/proof_test.go b/scripts/verify-email-storage-canary/proof_test.go index e0a9487039..3f110df448 100644 --- a/scripts/verify-email-storage-canary/proof_test.go +++ b/scripts/verify-email-storage-canary/proof_test.go @@ -13,12 +13,12 @@ func TestDispatchAuthorization(t *testing.T) { } for key, bad := range map[string]string{"GITHUB_REPOSITORY": "other/platform", "GITHUB_REF": "refs/pull/1/merge", "GITHUB_EVENT_NAME": "pull_request", "GITHUB_RUN_ATTEMPT": "2", "GITHUB_RUN_ID": "1/../../private-mail", "GITHUB_SHA": "main", "EMAIL_CANARY_CONFIRM": "yes", "RUNNER_ENVIRONMENT": "self-hosted"} { t.Run(key, func(t *testing.T) { - copy := map[string]string{} + clone := map[string]string{} for k, v := range valid { - copy[k] = v + clone[k] = v } - copy[key] = bad - if _, err := authorize(func(k string) string { return copy[k] }); err == nil { + clone[key] = bad + if _, err := authorize(func(k string) string { return clone[k] }); err == nil { t.Fatal("unsafe dispatch accepted") } }) diff --git a/scripts/verify-longhorn-migration-headroom/main.go b/scripts/verify-longhorn-migration-headroom/main.go index b911586012..537d222d54 100644 --- a/scripts/verify-longhorn-migration-headroom/main.go +++ b/scripts/verify-longhorn-migration-headroom/main.go @@ -1,3 +1,5 @@ +// Command verify-longhorn-migration-headroom reads the Hetzner server +// inventory between two Node reads to observe temporary migration headroom. package main import ( diff --git a/scripts/verify-wedding-backup-bootstrap/main.go b/scripts/verify-wedding-backup-bootstrap/main.go index 92654c3157..d5df43085c 100644 --- a/scripts/verify-wedding-backup-bootstrap/main.go +++ b/scripts/verify-wedding-backup-bootstrap/main.go @@ -1,3 +1,5 @@ +// Command verify-wedding-backup-bootstrap proves the Wedding backup bootstrap +// on a disposable fixture namespace of the existing cluster. package main import ( @@ -15,7 +17,7 @@ import ( type object = map[string]any type recipe struct{ push, pull, store, minio object } -var refused = errors.New("safety check failed") +var errRefused = errors.New("safety check failed") func at(o object, keys ...string) any { var v any = o @@ -65,28 +67,28 @@ var uuid = regexp.MustCompile(`^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$`) func readObject(filename string) (object, error) { a, err := filepath.Abs(filename) if err != nil { - return nil, refused + return nil, errRefused } - real, err := filepath.EvalSymlinks(a) - if err != nil || real != a { - return nil, refused + resolved, err := filepath.EvalSymlinks(a) + if err != nil || resolved != a { + return nil, errRefused } s, err := os.Lstat(a) if err != nil || !s.Mode().IsRegular() || s.Size() < 1 || s.Size() > 262144 { - return nil, refused + return nil, errRefused } b, err := os.ReadFile(a) if err != nil { - return nil, refused + return nil, errRefused } d := yaml.NewDecoder(bytes.NewReader(b)) var o object if d.Decode(&o) != nil || o == nil { - return nil, refused + return nil, errRefused } var extra any if d.Decode(&extra) != io.EOF { - return nil, refused + return nil, errRefused } return o, nil } @@ -105,7 +107,7 @@ func loadRecipe(root string) (recipe, error) { } func validateRecipe(r recipe) error { if str(r.push, "kind") != "PushSecret" || str(r.push, "metadata", "name") != "seed-wedding-db-backup-r2" || str(r.push, "metadata", "namespace") != "flux-system" || str(r.push, "spec", "refreshInterval") != "1h" || str(r.push, "spec", "selector", "secret", "name") != "wedding-db-backup-r2-bootstrap" { - return refused + return errRefused } wantPush := []any{} wantPull := []any{} @@ -114,31 +116,31 @@ func validateRecipe(r recipe) error { wantPull = append(wantPull, object{"secretKey": pair[1], "remoteRef": object{"key": dedicatedPath, "property": pair[0]}}) } if !reflect.DeepEqual(at(r.push, "spec", "data"), wantPush) || !reflect.DeepEqual(at(r.push, "spec", "secretStoreRefs"), []any{object{"name": "openbao", "kind": "ClusterSecretStore"}}) { - return refused + return errRefused } if str(r.pull, "kind") != "ExternalSecret" || str(r.pull, "metadata", "namespace") != "wedding-app" || str(r.pull, "metadata", "name") != projectedSecret || str(r.pull, "spec", "refreshInterval") != "1h" || str(r.pull, "spec", "target", "name") != projectedSecret || str(r.pull, "spec", "target", "creationPolicy") != "Owner" || !reflect.DeepEqual(at(r.pull, "spec", "secretStoreRef"), object{"name": "openbao", "kind": "ClusterSecretStore"}) || !reflect.DeepEqual(at(r.pull, "spec", "data"), wantPull) { - return refused + return errRefused } if !reflect.DeepEqual(at(r.pull, "spec", "target", "template", "data"), object{"ACCESS_KEY_ID": "{{ .ACCESS_KEY_ID }}", "SECRET_ACCESS_KEY": "{{ .SECRET_ACCESS_KEY }}", "REGION": "auto"}) { - return refused + return errRefused } refs := object{} for field, key := range map[string]string{"accessKeyId": "ACCESS_KEY_ID", "secretAccessKey": "SECRET_ACCESS_KEY", "region": "REGION"} { refs[field] = object{"name": projectedSecret, "key": key} } if str(r.store, "kind") != "ObjectStore" || str(r.store, "metadata", "name") != "wedding-db-dedicated" || str(r.store, "spec", "configuration", "destinationPath") != "s3://wedding-db-backups/cnpg/wedding-db" || str(r.store, "spec", "configuration", "endpointURL") != "${r2_endpoint}" || !reflect.DeepEqual(at(r.store, "spec", "configuration", "s3Credentials"), refs) { - return refused + return errRefused } containers, _ := at(r.minio, "spec", "template", "spec", "containers").([]any) if str(r.minio, "kind") != "Deployment" || len(containers) != 1 { - return refused + return errRefused } c, ok := containers[0].(map[string]any) if !ok || !regexp.MustCompile(`^docker\.io/chrislusf/seaweedfs:[0-9]+\.[0-9]+@sha256:[a-f0-9]{64}$`).MatchString(str(c, "image")) { - return refused + return errRefused } if at(c, "env") != nil || at(c, "envFrom") != nil || !reflect.DeepEqual(at(c, "command"), []any{"/bin/sh", "-ec"}) || !reflect.DeepEqual(at(c, "args"), []any{s3ServerScript}) { - return refused + return errRefused } return nil } @@ -157,7 +159,7 @@ func meta(name, ns, run string) object { } func fixture(run string, r recipe, access, password string) ([]object, error) { if !digits.MatchString(run) || validateRecipe(r) != nil || !regexp.MustCompile(`^[A-Za-z0-9]{16,64}$`).MatchString(access) || !regexp.MustCompile(`^[A-Za-z0-9]{20,128}$`).MatchString(password) || access == password { - return nil, refused + return nil, errRefused } ns := "wedding-bootstrap-" + run nsm := meta(ns, "", run) @@ -208,7 +210,7 @@ ui = false func invocation(env func(string) string) (string, error) { run := env("GITHUB_RUN_ID") if env("GITHUB_REPOSITORY") != "devantler-tech/platform" || env("GITHUB_EVENT_NAME") != "workflow_dispatch" || env("GITHUB_REF") != "refs/heads/main" || env("GITHUB_RUN_ATTEMPT") != "1" || env("WEDDING_BOOTSTRAP_CONFIRM") != "verify-wedding-backup-bootstrap" || !digits.MatchString(run) { - return "", refused + return "", errRefused } return run, nil } diff --git a/scripts/verify-wedding-backup-bootstrap/operator.go b/scripts/verify-wedding-backup-bootstrap/operator.go index b595a7528a..543283bdb7 100644 --- a/scripts/verify-wedding-backup-bootstrap/operator.go +++ b/scripts/verify-wedding-backup-bootstrap/operator.go @@ -18,7 +18,7 @@ func prove(k client, r recipe) (err error) { for _, target := range [][3]string{{"", "namespaces", ns}, {"external-secrets", "ciliumnetworkpolicies", ns}} { o, e := k.get(target[0], target[1], target[2]) if e != nil || o != nil { - return refused + return errRefused } } access, e := randomCredential(12) @@ -43,12 +43,12 @@ func prove(k client, r recipe) (err error) { clean := k clean.ctx = ctx if cleanup(clean, nsUID) != nil { - err = refused + err = errRefused } }() n, e := k.get("", "namespaces", ns) if e != nil || !owned(n, ns, "", k.run) { - return refused + return errRefused } nsUID = str(n, "metadata", "uid") // Install isolation before a Pod can start. @@ -75,7 +75,7 @@ func prove(k client, r recipe) (err error) { } for _, name := range []string{"openbao", "minio"} { if e = waitFor(k, func() (bool, error) { o, e := k.get(ns, "pods", name); return ready(o), e }); e != nil { - return refused + return errRefused } } url, stop, e := forward(k.ctx, k.run) @@ -83,7 +83,7 @@ func prove(k client, r recipe) (err error) { return e } defer stop() - b := bao{url: url, ctx: k.ctx, http: &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return refused }}} + b := bao{url: url, ctx: k.ctx, http: &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return errRefused }}} token, e := b.initialize() if e != nil { return e @@ -132,7 +132,7 @@ func prove(k client, r recipe) (err error) { } data, _ := at(kv, "data", "data").(map[string]any) if len(data) != 2 || str(data, "access_key_id") != access || str(data, "secret_access_key") != password { - return false, refused + return false, errRefused } currentSecret, currentPush, currentExternal = s, push, pull return true, nil @@ -143,11 +143,11 @@ func prove(k client, r recipe) (err error) { initialUID := str(currentSecret, "metadata", "uid") initialTime, e := time.Parse(time.RFC3339Nano, str(currentPush, "status", "refreshTime")) if e != nil { - return refused + return errRefused } source, e := k.get(ns, "secrets", "wedding-db-backup-r2-bootstrap") if e != nil || !owned(source, "wedding-db-backup-r2-bootstrap", ns, k.run) { - return refused + return errRefused } initialSource := source fmt.Fprintln(os.Stderr, "Bootstrap proof: initial delivery verified; removing fixture copies") @@ -168,7 +168,7 @@ func prove(k client, r recipe) (err error) { } refresh, e := time.Parse(time.RFC3339Nano, str(currentPush, "status", "refreshTime")) if e != nil { - return false, refused + return false, errRefused } return str(currentSecret, "metadata", "uid") != initialUID && refresh.After(initialTime) && uuid.MatchString(str(currentExternal, "metadata", "uid")), nil }); e != nil { @@ -176,7 +176,7 @@ func prove(k client, r recipe) (err error) { } source, e = k.get(ns, "secrets", "wedding-db-backup-r2-bootstrap") if e != nil || !unchangedSource(initialSource, source) { - return refused + return errRefused } fmt.Fprintln(os.Stderr, "Bootstrap proof: periodic repair verified; testing projected credentials") p, e := probePod(k.run, r) @@ -185,7 +185,7 @@ func prove(k client, r recipe) (err error) { } server, e := k.get(ns, "pods", "minio") if e != nil || !unchangedPeerServer(server, server, k.run) || bindPeerAddress(p, server, k.run) != nil { - return refused + return errRefused } if e = k.create(p); e != nil { return e @@ -199,11 +199,11 @@ func prove(k client, r recipe) (err error) { case "Succeeded": after, e := k.get(ns, "pods", "minio") if e != nil || !unchangedPeerServer(server, after, k.run) { - return false, refused + return false, errRefused } return true, nil case "Failed": - return false, refused + return false, errRefused } return false, nil }) diff --git a/scripts/verify-wedding-backup-bootstrap/proof.go b/scripts/verify-wedding-backup-bootstrap/proof.go index 929bdf61df..c4645f6a61 100644 --- a/scripts/verify-wedding-backup-bootstrap/proof.go +++ b/scripts/verify-wedding-backup-bootstrap/proof.go @@ -36,7 +36,7 @@ func projection(s, es object, run, access, password string) bool { func probePod(run string, r recipe) (object, error) { if !digits.MatchString(run) || validateRecipe(r) != nil { - return nil, refused + return nil, errRefused } destination := strings.TrimPrefix(str(r.store, "spec", "configuration", "destinationPath"), "s3://") script := `set -eu @@ -70,7 +70,7 @@ cmp /tmp/sentinel /tmp/readback func randomCredential(size int) (string, error) { b := make([]byte, size) if _, err := rand.Read(b); err != nil { - return "", refused + return "", errRefused } return hex.EncodeToString(b), nil } @@ -78,7 +78,7 @@ func randomCredential(size int) (string, error) { func bindPeerAddress(p, server object, run string) error { ip := net.ParseIP(str(server, "status", "podIP")) if !owned(server, "minio", "wedding-bootstrap-"+run, run) || ip == nil || !ip.IsGlobalUnicast() { - return refused + return errRefused } c := at(p, "spec", "containers").([]any)[0].(map[string]any) c["env"] = append(at(c, "env").([]any), object{"name": "S3_POD_IP", "value": ip.String()}) diff --git a/scripts/verify-wedding-backup-bootstrap/runtime.go b/scripts/verify-wedding-backup-bootstrap/runtime.go index 55bd1ee7bc..47c29b345c 100644 --- a/scripts/verify-wedding-backup-bootstrap/runtime.go +++ b/scripts/verify-wedding-backup-bootstrap/runtime.go @@ -24,7 +24,7 @@ type client struct { func esoPolicy(run, nsUID string) (object, error) { if !digits.MatchString(run) || !uuid.MatchString(nsUID) { - return nil, refused + return nil, errRefused } ns := "wedding-bootstrap-" + run m := meta(ns, "external-secrets", run) @@ -34,7 +34,7 @@ func esoPolicy(run, nsUID string) (object, error) { func (k client) call(ns string, input []byte, args ...string) ([]byte, error) { if !digits.MatchString(k.run) || (ns != "" && ns != "wedding-bootstrap-"+k.run && ns != "external-secrets") { - return nil, refused + return nil, errRefused } prefix := []string{"--context", "admin@prod", "--request-timeout=30s"} if ns != "" { @@ -45,24 +45,24 @@ func (k client) call(ns string, input []byte, args ...string) ([]byte, error) { func (k client) get(ns, kind, name string) (object, error) { b, err := k.call(ns, nil, "get", kind, name, "--ignore-not-found=true", "-o", "json") if err != nil { - return nil, refused + return nil, errRefused } if len(bytes.TrimSpace(b)) == 0 { return nil, nil } var o object if json.Unmarshal(b, &o) != nil || o == nil { - return nil, refused + return nil, errRefused } return o, nil } func (k client) create(o object) error { - if str(o, "metadata", "namespace") != "wedding-bootstrap-"+k.run && !(str(o, "kind") == "Namespace" && str(o, "metadata", "name") == "wedding-bootstrap-"+k.run) && !(str(o, "kind") == "CiliumNetworkPolicy" && str(o, "metadata", "namespace") == "external-secrets" && str(o, "metadata", "name") == "wedding-bootstrap-"+k.run) { - return refused + if str(o, "metadata", "namespace") != "wedding-bootstrap-"+k.run && (str(o, "kind") != "Namespace" || str(o, "metadata", "name") != "wedding-bootstrap-"+k.run) && (str(o, "kind") != "CiliumNetworkPolicy" || str(o, "metadata", "namespace") != "external-secrets" || str(o, "metadata", "name") != "wedding-bootstrap-"+k.run) { + return errRefused } b, err := json.Marshal(o) if err != nil { - return refused + return errRefused } _, err = k.call("", b, "create", "-f", "-", "-o", "name") return err @@ -79,31 +79,31 @@ func removeOwned(k client, ns, kind, name, expectedUID string) error { return nil } if !owned(o, name, ns, k.run) || (expectedUID != "" && str(o, "metadata", "uid") != expectedUID) { - return refused + return errRefused } var url string switch kind { case "namespaces": if ns != "" || name != "wedding-bootstrap-"+k.run { - return refused + return errRefused } url = "/api/v1/namespaces/" + name case "secrets": if ns != "wedding-bootstrap-"+k.run || name != projectedSecret { - return refused + return errRefused } url = "/api/v1/namespaces/" + ns + "/secrets/" + name case "ciliumnetworkpolicies": if ns != "external-secrets" || name != "wedding-bootstrap-"+k.run { - return refused + return errRefused } url = "/apis/cilium.io/v2/namespaces/" + ns + "/ciliumnetworkpolicies/" + name default: - return refused + return errRefused } body, _ := json.Marshal(object{"apiVersion": "v1", "kind": "DeleteOptions", "preconditions": object{"uid": str(o, "metadata", "uid")}, "propagationPolicy": "Foreground"}) if _, err = k.call("", body, "delete", "--raw="+url, "-f", "-"); err != nil { - return refused + return errRefused } // ESO can recreate a Secret immediately. Wait for the deleted UID, rather // than waiting forever for its replacement to disappear by name. @@ -119,7 +119,7 @@ func cleanup(k client, nsUID string) error { return err } if n != nil && (!owned(n, ns, "", k.run) || (nsUID != "" && str(n, "metadata", "uid") != nsUID)) { - return refused + return errRefused } if n != nil { nsUID = str(n, "metadata", "uid") @@ -131,11 +131,11 @@ func cleanup(k client, nsUID string) error { if p != nil { refs, _ := at(p, "metadata", "ownerReferences").([]any) if !owned(p, ns, "external-secrets", k.run) || len(refs) != 1 { - return refused + return errRefused } owner, ok := refs[0].(map[string]any) if !ok || str(owner, "kind") != "Namespace" || str(owner, "name") != ns || !uuid.MatchString(str(owner, "uid")) || (nsUID != "" && str(owner, "uid") != nsUID) { - return refused + return errRefused } if err = removeOwned(k, "external-secrets", "ciliumnetworkpolicies", ns, str(p, "metadata", "uid")); err != nil { return err @@ -147,7 +147,7 @@ func cleanup(k client, nsUID string) error { for _, target := range [][3]string{{"", "namespaces", ns}, {"external-secrets", "ciliumnetworkpolicies", ns}} { o, err := k.get(target[0], target[1], target[2]) if err != nil || o != nil { - return refused + return errRefused } } return nil @@ -161,7 +161,7 @@ type bao struct { func (b bao) request(method, path, token string, input object, want int) (object, error) { if !regexp.MustCompile(`^http://127\.0\.0\.1:[1-9][0-9]{0,4}$`).MatchString(b.url) || !strings.HasPrefix(path, "/v1/") { - return nil, refused + return nil, errRefused } payload, _ := json.Marshal(input) if input == nil { @@ -169,7 +169,7 @@ func (b bao) request(method, path, token string, input object, want int) (object } req, err := http.NewRequestWithContext(b.ctx, method, b.url+path, bytes.NewReader(payload)) if err != nil { - return nil, refused + return nil, errRefused } req.Header.Set("Content-Type", "application/json") if token != "" { @@ -177,19 +177,19 @@ func (b bao) request(method, path, token string, input object, want int) (object } resp, err := b.http.Do(req) if err != nil { - return nil, refused + return nil, errRefused } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(io.LimitReader(resp.Body, 1048577)) if err != nil || len(body) > 1048576 || resp.StatusCode != want { - return nil, refused + return nil, errRefused } if len(bytes.TrimSpace(body)) == 0 { return object{}, nil } var result object if json.Unmarshal(body, &result) != nil { - return nil, refused + return nil, errRefused } return result, nil } @@ -201,15 +201,15 @@ func (b bao) initialize() (string, error) { keys, _ := init["keys_base64"].([]any) root := str(init, "root_token") if len(keys) != 1 || root == "" { - return "", refused + return "", errRefused } key, ok := keys[0].(string) if !ok || key == "" { - return "", refused + return "", errRefused } unsealed, err := b.request("PUT", "/v1/sys/unseal", "", object{"key": key}, 200) if err != nil || unsealed["sealed"] != false { - return "", refused + return "", errRefused } if _, err = b.request("POST", "/v1/sys/mounts/secret", root, object{"type": "kv", "options": object{"version": "2"}}, 204); err != nil { return "", err @@ -229,14 +229,14 @@ path "auth/token/lookup-self" { capabilities = ["read"] }` } value := str(token, "auth", "client_token") if value == "" { - return "", refused + return "", errRefused } return value, nil } func forward(ctx context.Context, run string) (string, func(), error) { if !digits.MatchString(run) { - return "", nil, refused + return "", nil, errRefused } fctx, cancel := context.WithCancel(ctx) cmd := exec.CommandContext(fctx, "kubectl", "--context", "admin@prod", "--namespace", "wedding-bootstrap-"+run, "port-forward", "--address=127.0.0.1", "pod/openbao", "0:8200") @@ -245,11 +245,11 @@ func forward(ctx context.Context, run string) (string, func(), error) { out, err := cmd.StdoutPipe() if err != nil { cancel() - return "", nil, refused + return "", nil, errRefused } if cmd.Start() != nil { cancel() - return "", nil, refused + return "", nil, errRefused } ports := make(chan string, 1) finished := make(chan struct{}) @@ -278,13 +278,13 @@ func forward(ctx context.Context, run string) (string, func(), error) { return "http://127.0.0.1:" + p, stop, nil case <-finished: cancel() - return "", nil, refused + return "", nil, errRefused case <-ctx.Done(): stop() - return "", nil, refused + return "", nil, errRefused case <-time.After(45 * time.Second): stop() - return "", nil, refused + return "", nil, errRefused } } func waitFor(k client, predicate func() (bool, error)) error { @@ -300,7 +300,7 @@ func waitFor(k client, predicate func() (bool, error)) error { } select { case <-ctx.Done(): - return refused + return errRefused case <-time.After(2 * time.Second): } } @@ -315,7 +315,6 @@ func ready(o object) bool { } return false } -func number(o object, keys ...string) int { n, _ := at(o, keys...).(float64); return int(n) } func newClient(ctx context.Context, run string) client { return client{ctx: ctx, run: run, command: func(ctx context.Context, args []string, input []byte) ([]byte, error) { @@ -326,7 +325,7 @@ func newClient(ctx context.Context, run string) client { var stdout bytes.Buffer cmd.Stdout = &limitedWriter{writer: &stdout, remaining: 1048576} if cmd.Run() != nil { - return nil, refused + return nil, errRefused } return stdout.Bytes(), nil }} diff --git a/scripts/verify-wedding-dedicated-restore/main.go b/scripts/verify-wedding-dedicated-restore/main.go index f3c690c9e0..7f4ad661ef 100644 --- a/scripts/verify-wedding-dedicated-restore/main.go +++ b/scripts/verify-wedding-dedicated-restore/main.go @@ -1,3 +1,5 @@ +// Command verify-wedding-dedicated-restore proves a dedicated Wedding database +// restore while preserving the live database. package main import ( @@ -29,7 +31,7 @@ type inventory struct { Latest string `json:"latest"` } -var refused = errors.New("safety check failed") +var errRefused = errors.New("safety check failed") var digits = regexp.MustCompile(`^[1-9][0-9]{0,19}$`) var uuid = regexp.MustCompile(`^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$`) var endpoint = regexp.MustCompile(`^https://([0-9a-f]{32}\.r2\.cloudflarestorage\.com)$`) @@ -63,36 +65,36 @@ func ready(o object, kind string) bool { func validateSource(c, s, b object) (source, error) { uid := str(c, "metadata", "uid") if str(c, "metadata", "name") != "wedding-db" || str(c, "metadata", "namespace") != "wedding-app" || !uuid.MatchString(uid) || value(c, "metadata", "deletionTimestamp") != nil || number(c, "metadata", "generation") < 1 || number(c, "spec", "instances") != 3 || number(c, "status", "readyInstances") != 3 || !ready(c, "Ready") || !ready(c, "ContinuousArchiving") { - return source{}, refused + return source{}, errRefused } p, _ := value(c, "spec", "plugins").([]any) if len(p) != 1 { - return source{}, refused + return source{}, errRefused } plugin, _ := p[0].(map[string]any) if plugin["name"] != "barman-cloud.cloudnative-pg.io" || plugin["enabled"] != true || plugin["isWALArchiver"] != true || str(plugin, "parameters", "barmanObjectName") != "wedding-db-dedicated" || str(plugin, "parameters", "serverName") != "wedding-db-20260909" { - return source{}, refused + return source{}, errRefused } if str(s, "metadata", "name") != "wedding-db-dedicated" || str(s, "metadata", "namespace") != "wedding-app" || !uuid.MatchString(str(s, "metadata", "uid")) || number(s, "metadata", "generation") < 1 || value(s, "metadata", "deletionTimestamp") != nil || str(s, "spec", "configuration", "destinationPath") != "s3://wedding-db-backups/cnpg/wedding-db" { - return source{}, refused + return source{}, errRefused } host := endpoint.FindStringSubmatch(str(s, "spec", "configuration", "endpointURL")) if len(host) != 2 { - return source{}, refused + return source{}, errRefused } want := object{} for field, key := range map[string]string{"accessKeyId": "ACCESS_KEY_ID", "secretAccessKey": "SECRET_ACCESS_KEY", "region": "REGION"} { want[field] = object{"name": credential, "key": key} } if !reflect.DeepEqual(value(s, "spec", "configuration", "s3Credentials"), want) { - return source{}, refused + return source{}, errRefused } if str(b, "metadata", "namespace") != "wedding-app" || str(b, "status", "phase") != "completed" || str(b, "spec", "cluster", "name") != "wedding-db" || str(b, "spec", "method") != "plugin" || str(b, "spec", "pluginConfiguration", "name") != "barman-cloud.cloudnative-pg.io" || str(b, "status", "pluginMetadata", "clusterUID") != uid || str(b, "status", "pluginMetadata", "name") != "barman-cloud.cloudnative-pg.io" || !regexp.MustCompile(`^[0-9]{8}T[0-9]{6}$`).MatchString(str(b, "status", "backupId")) { - return source{}, refused + return source{}, errRefused } image, size := str(c, "spec", "imageName"), str(c, "spec", "storage", "size") if !regexp.MustCompile(`^ghcr\.io/cloudnative-pg/postgresql:18\.[0-9]+-[a-z0-9-]+$`).MatchString(image) || !regexp.MustCompile(`^[1-9][0-9]*(Mi|Gi)$`).MatchString(size) || str(c, "spec", "storage", "storageClass") != "longhorn-wffc" { - return source{}, refused + return source{}, errRefused } return source{uid, str(s, "metadata", "uid"), image, size, host[1], str(b, "status", "backupId"), number(c, "metadata", "generation"), number(s, "metadata", "generation")}, nil } @@ -136,18 +138,18 @@ func resources(run string, s source) []object { // explicitly documented five-minute allowance for concurrently committed writes. func compare(before, recovered, after inventory) error { if before.Pairs < 1 || before.Guests < 1 || recovered.Pairs != before.Pairs || recovered.Pairs != after.Pairs || recovered.Guests != before.Guests || recovered.Guests != after.Guests || recovered.Answers < 1 { - return refused + return errRefused } for _, triple := range [][3]int{{before.Answers, recovered.Answers, after.Answers}, {before.Bookings, recovered.Bookings, after.Bookings}} { if triple[1] < min(triple[0], triple[2]) || triple[1] > max(triple[0], triple[2]) { - return refused + return errRefused } } b, eb := time.Parse(time.RFC3339Nano, before.Latest) r, er := time.Parse(time.RFC3339Nano, recovered.Latest) a, ea := time.Parse(time.RFC3339Nano, after.Latest) if eb != nil || er != nil || ea != nil || r.After(a) || b.Sub(r) > 5*time.Minute { - return refused + return errRefused } return nil } @@ -167,21 +169,21 @@ func (k client) call(ns string, input []byte, args ...string) ([]byte, error) { func (k client) get(ns, kind, name string) (object, error) { b, err := k.call(ns, nil, "get", kind, name, "--ignore-not-found=true", "-o", "json") if err != nil { - return nil, refused + return nil, errRefused } if len(bytes.TrimSpace(b)) == 0 { return nil, nil } var o object if json.Unmarshal(b, &o) != nil || o == nil { - return nil, refused + return nil, errRefused } return o, nil } func (k client) create(o object) error { b, err := json.Marshal(o) if err != nil { - return refused + return errRefused } _, err = k.call("", b, "create", "-f", "-", "-o", "json") return err @@ -189,13 +191,13 @@ func (k client) create(o object) error { func (k client) list(ns, kind string) ([]object, error) { b, err := k.call(ns, nil, "get", kind, "-o", "json") if err != nil { - return nil, refused + return nil, errRefused } var list struct { Items []object `json:"items"` } if json.Unmarshal(b, &list) != nil || list.Items == nil { - return nil, refused + return nil, errRefused } return list.Items, nil } @@ -204,12 +206,12 @@ const inventorySQL = `BEGIN READ ONLY; SELECT json_build_object('pairs',(SELECT func (k client) inventory(ns, primary string) (inventory, error) { if !regexp.MustCompile(`^(wedding-db|restore)-[1-9][0-9]*$`).MatchString(primary) { - return inventory{}, refused + return inventory{}, errRefused } b, err := k.call(ns, nil, "exec", primary, "--container=postgres", "--", "psql", "--username=postgres", "--dbname=wedding", "--set=ON_ERROR_STOP=1", "--tuples-only", "--no-align", "--quiet", "--command="+inventorySQL) var result inventory if err != nil || json.Unmarshal(bytes.TrimSpace(b), &result) != nil { - return inventory{}, refused + return inventory{}, errRefused } return result, nil } @@ -224,15 +226,15 @@ func cleanup(k client, run, expectedUID string) error { } if o != nil { if !owns(o, run) || (expectedUID != "" && str(o, "metadata", "uid") != expectedUID) { - return refused + return errRefused } // Raw DELETE carries a UID precondition; a same-name replacement is safe. body, _ := json.Marshal(object{"apiVersion": "v1", "kind": "DeleteOptions", "preconditions": object{"uid": str(o, "metadata", "uid")}, "propagationPolicy": "Foreground"}) if _, err = k.call("", body, "delete", "--raw=/api/v1/namespaces/"+ns, "-f", "-"); err != nil { - return refused + return errRefused } if _, err = k.call("", nil, "wait", "--for=delete", "namespace/"+ns, "--timeout=10m"); err != nil { - return refused + return errRefused } } // Namespace deletion includes the temporary policies, Secret, Cluster and @@ -247,31 +249,31 @@ func cleanup(k client, run, expectedUID string) error { if str(pv, "spec", "claimRef", "namespace") == ns { remaining = true if str(pv, "spec", "persistentVolumeReclaimPolicy") != "Delete" { - return refused + return errRefused } } } if !remaining { remainingNS, err := k.get("", "namespaces", ns) if err != nil || remainingNS != nil { - return refused + return errRefused } return nil } select { case <-k.ctx.Done(): - return refused + return errRefused case <-time.After(5 * time.Second): } } - return refused + return errRefused } func prove(k client, run string) (result object, err error) { ns := "wedding-restore-" + run existing, err := k.get("", "namespaces", ns) if err != nil || existing != nil { - return nil, refused + return nil, errRefused } c, err := k.get("wedding-app", "clusters.postgresql.cnpg.io", "wedding-db") if err != nil { @@ -295,15 +297,15 @@ func prove(k client, run string) (result object, err error) { } // Flush the current segment so the restore can observe recent committed data. if _, err = k.call("wedding-app", nil, "exec", str(c, "status", "currentPrimary"), "--container=postgres", "--", "psql", "--username=postgres", "--dbname=postgres", "--set=ON_ERROR_STOP=1", "--quiet", "--command=SELECT pg_switch_wal();"); err != nil { - return nil, refused + return nil, errRefused } items := resources(run, state) if err = k.create(items[0]); err != nil { - return nil, refused + return nil, errRefused } created, err := k.get("", "namespaces", ns) if err != nil || !owns(created, run) { - return nil, refused + return nil, errRefused } nsUID := str(created, "metadata", "uid") defer func() { @@ -317,46 +319,46 @@ func prove(k client, run string) (result object, err error) { } }() if err = k.create(items[1]); err != nil { - return nil, refused + return nil, errRefused } secrets, err := k.list(ns, "secrets") if err != nil || len(secrets) != 0 { - return nil, refused + return nil, errRefused } secret, err := k.get("wedding-app", "secrets", credential) if err != nil { - return nil, refused + return nil, errRefused } data, _ := value(secret, "data").(map[string]any) if len(data) != 3 || str(secret, "metadata", "name") != credential { - return nil, refused + return nil, errRefused } for _, key := range []string{"ACCESS_KEY_ID", "SECRET_ACCESS_KEY", "REGION"} { if v, ok := data[key].(string); !ok || v == "" { - return nil, refused + return nil, errRefused } } if err = k.create(object{"apiVersion": "v1", "kind": "Secret", "metadata": metadata(credential, ns, run), "type": "Opaque", "data": data}); err != nil { - return nil, refused + return nil, errRefused } // No value or Secret body reaches stdout, stderr or the proof receipt. for _, item := range items[2:] { if err = k.create(item); err != nil { - return nil, refused + return nil, errRefused } } if _, err = k.call(ns, nil, "wait", "--for=condition=Ready", "cluster.postgresql.cnpg.io/restore", "--timeout=30m"); err != nil { - return nil, refused + return nil, errRefused } restored, err := k.get(ns, "clusters.postgresql.cnpg.io", "restore") if err != nil || number(restored, "status", "readyInstances") != 1 || !ready(restored, "Ready") || value(restored, "spec", "plugins") != nil { - return nil, refused + return nil, errRefused } // Resolve the live service first and prove the probe works locally. Only // PostgreSQL's no-response result satisfies the negative connectivity check. const isolationProbe = `set -eu; pg_isready -h 127.0.0.1 -p 5432 -t 3 >/dev/null; getent ahostsv4 wedding-db-rw.wedding-app.svc.cluster.local >/dev/null; set +e; pg_isready -h wedding-db-rw.wedding-app.svc.cluster.local -p 5432 -t 3 >/dev/null; rc=$?; test "$rc" -eq 2` if _, err = k.call(ns, nil, "exec", str(restored, "status", "currentPrimary"), "--container=postgres", "--", "sh", "-c", isolationProbe); err != nil { - return nil, refused + return nil, errRefused } recovered, err := k.inventory(ns, str(restored, "status", "currentPrimary")) if err != nil { @@ -372,7 +374,7 @@ func prove(k client, run string) (result object, err error) { } stateAfter, err := validateSource(cAfter, sAfter, b) if err != nil || state != stateAfter { - return nil, refused + return nil, errRefused } after, err := k.inventory("wedding-app", str(cAfter, "status", "currentPrimary")) if err != nil { @@ -387,7 +389,7 @@ func prove(k client, run string) (result object, err error) { func invocation(env func(string) string) (string, error) { run := env("GITHUB_RUN_ID") if env("GITHUB_REPOSITORY") != "devantler-tech/platform" || env("GITHUB_EVENT_NAME") != "workflow_dispatch" || env("GITHUB_REF") != "refs/heads/main" || env("GITHUB_RUN_ATTEMPT") != "1" || env("WEDDING_RESTORE_CONFIRM") != "verify-wedding-dedicated-restore" || !digits.MatchString(run) { - return "", refused + return "", errRefused } return run, nil } @@ -402,7 +404,7 @@ func main() { if len(os.Args) == 2 && os.Args[1] == "--cleanup" { mode = "cleanup" } else if len(os.Args) != 1 { - return refused + return errRefused } ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() @@ -415,7 +417,7 @@ func main() { cmd.Stderr = &stderr out, err := cmd.Output() if err != nil || len(out) > 4*1024*1024 { - return nil, refused + return nil, errRefused } return out, nil }} From 2094e1d2f8d6870c2d16c925404d927401417457 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 22:11:32 +0100 Subject: [PATCH 04/19] fix(lint): run revive's default rules except file-by-file package comments MegaLinter passes revive a list of files, so revive asks for a package comment in every file of a package that already has one: 166 findings. Keep every default rule and turn off only package-comments, as the maintainer decided on devantler-tech/platform#5852. Every package already carries its own package comment. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- revive.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 revive.toml diff --git a/revive.toml b/revive.toml new file mode 100644 index 0000000000..99bf9318f0 --- /dev/null +++ b/revive.toml @@ -0,0 +1,16 @@ +# revive configuration, read by MegaLinter's GO_REVIVE. +# +# Every default rule stays on except package-comments. MegaLinter hands revive a list of files +# rather than packages, so revive treats each file as a package of its own and asks for a package +# comment in every file of a package that already has one. Every package here carries its own +# package comment; satisfying the file-by-file reading would mean duplicating it into each file, +# and `go doc` would then print every copy. Maintainer decision, 3 November 2026, devantler-tech/platform#5852. +# +# severity and confidence restate revive's built-in defaults, which apply only without a config file. + +severity = "warning" +confidence = 0.8 +enable-default-rules = true + +[rule.package-comments] +Disabled = true From 3563cff8c934c3a42922d9e9d912cb58a3c7adbc Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 22:45:31 +0100 Subject: [PATCH 05/19] docs(agents): keep the repository passing the org Go workflow GitHub's detected language decides whether the organization's required Go workflow applies, and Go and Shell are within a few kilobytes of each other here. Record that the repository keeps passing it either way. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f7be729b07..f8a39ffd0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ ksail cluster list # existing Talos clusters ## Validation -The repository has static manifest checks, Go tests and linters, and MegaLinter. **Never run a cluster for maintenance.** For Go changes, run the affected packages' tests from the repository root (for example, `go test ./scripts/analyze-cri-logs`) and the relevant offline script regressions. The unconditional required `validate-go-regressions` job in [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) checks every tracked module with `bash scripts/check-go-modules-tidy.sh` before running the complete root-module Go suite. Its offline controls are `bash scripts/tests/test-go-modules-tidy.sh`. The guard leaves module files unchanged and names the module needing `go mod tidy`; it also refuses failed or empty discovery. The organization Go workflow's language selector does not enroll this primarily Shell repository. MegaLinter's scope and dispositions live in [`.mega-linter.yml`](.mega-linter.yml). For manifest changes, validate with: +The repository has static manifest checks, Go tests and linters, and MegaLinter. **Never run a cluster for maintenance.** For Go changes, run the affected packages' tests from the repository root (for example, `go test ./scripts/analyze-cri-logs`) and the relevant offline script regressions. The unconditional required `validate-go-regressions` job in [`.github/workflows/ci.yaml`](.github/workflows/ci.yaml) checks every tracked module with `bash scripts/check-go-modules-tidy.sh` before running the complete root-module Go suite. Its offline controls are `bash scripts/tests/test-go-modules-tidy.sh`. The guard leaves module files unchanged and names the module needing `go mod tidy`; it also refuses failed or empty discovery. The organization's required Go workflow (`validate-go-project.yaml` in devantler-tech/.github) applies whenever GitHub detects this repository's language as Go, and Go and Shell are within a few kilobytes of each other, so enrolment can flip with any change. Keep the repository passing that workflow whichever language is detected: its golangci-lint, dead-code, test, race-coverage and whole-repository MegaLinter jobs (#5852). MegaLinter's scope and dispositions live in [`.mega-linter.yml`](.mega-linter.yml). For manifest changes, validate with: ```bash # Preferred when KSail is installed: schema-aware validation with Flux variable From 93586cfd52e185b5ba4c7fc2a295a68222b55c25 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 23:05:18 +0100 Subject: [PATCH 06/19] fix(lint): attach the Betterleaks allowlists to their rules Betterleaks 1.7 attaches global [[allowlists]] with targetRules only in the top-level config. MegaLinter's generated config extends this file, so the targeted allowlists were dropped and their six findings stayed. Rule-level allowlists merge into the default rules on extend. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- .betterleaks.toml | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/.betterleaks.toml b/.betterleaks.toml index 4d6b6987c5..68af4c591f 100644 --- a/.betterleaks.toml +++ b/.betterleaks.toml @@ -1,23 +1,30 @@ # Betterleaks configuration. MegaLinter's REPOSITORY_BETTERLEAKS extends this file, and this file -# extends the upstream default ruleset. Each allowlist below names one rule and one shape that is -# not secret material, so a real credential anywhere else, or of another shape, still fails. +# extends the upstream default ruleset. Each allowlist below sits on one rule and names one shape +# that is not secret material, so a real credential anywhere else, or of another shape, still fails. # Test fixtures that can carry a comment use an inline `betterleaks:allow` instead. +# +# The allowlists are rule-level ([[rules.allowlists]]), not global [[allowlists]] with targetRules: +# Betterleaks 1.7 attaches targetRules only in the top-level config, and MegaLinter's generated +# config extends this one, so targeted global allowlists here are silently dropped. [extend] useDefault = true -[[allowlists]] +[[rules]] +id = "curl-auth-user" + +[[rules.allowlists]] description = "Container identities: `docker run --user :` near a curl call is a numeric user, not a curl credential." -targetRules = ["curl-auth-user"] regexes = ['''^[0-9]+:[0-9]+$'''] -[[allowlists]] +[[rules.allowlists]] description = "Disposable test accounts: the mail-server tests sign in as `fixture` to a throwaway local container." -targetRules = ["curl-auth-user"] regexes = ['''^fixture:[a-z-]+$'''] -[[allowlists]] +[[rules]] +id = "generic-api-key" + +[[rules.allowlists]] description = "Go module checksums: an `h1:` go.sum line, as quoted in a vendored go.sum diff, is a public hash." -targetRules = ["generic-api-key"] regexTarget = "line" -regexes = ['''^[+ -]?\S+ v\S+ h1:[A-Za-z0-9+/]{43}=$'''] +regexes = ['''^[+ -]?\S+ v\S+ h1:[A-Za-z0-9+/]{43}=\s*$'''] From fc985253d586d7215e07adbe7dcffe0fbae7f983 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 22:56:43 +0100 Subject: [PATCH 07/19] fix(deps): bump vulnerable Go modules in every tracked module MegaLinter's grype, osv-scanner and trivy scans flag Go module versions with published advisories across the repository's Go modules. Bump each to its fixed release in every module that pins it: - golang.org/x/net to v0.62.0 (GO-2026-6603/6610/6611/6612/6617/ 6860/6866), which raises golang.org/x/text to v0.43.0 (GO-2026-6629) through minimal version selection. - golang.org/x/crypto to v0.58.1-0.20261009181718-2f1f834330b2, the fixed version GO-2026-5932 names (the commit that removes the unmaintained openpgp package; no tagged release carries it yet). Nothing here imports openpgp, so every module still builds. - github.com/google/cel-go to v0.30.0 (GO-2026-6094 / GHSA-gcjh-h69q-9w9g) in the three native admission proofs. The audited controller renderer keeps Helm v4.2.4 and Go 1.26.8, so the build guard's SDK pin and build metadata are unchanged. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- go.mod | 12 +++---- go.sum | 28 ++++++++-------- .../testdata/retirement-fence-proof/go.mod | 15 ++++----- .../testdata/retirement-fence-proof/go.sum | 30 ++++++++--------- scripts/render-helm-chart/go.mod | 12 +++---- scripts/render-helm-chart/go.sum | 32 +++++++++---------- .../tests/gateway-receiver-admission/go.mod | 12 +++---- .../tests/gateway-receiver-admission/go.sum | 24 +++++++------- scripts/tests/kyverno-auth-reviews/api/go.mod | 8 ++--- scripts/tests/kyverno-auth-reviews/api/go.sum | 28 ++++++++-------- .../testdata/native-proof/go.mod | 15 ++++----- .../testdata/native-proof/go.sum | 30 ++++++++--------- .../tests/openbao-strategy-transition/go.mod | 10 +++--- .../tests/openbao-strategy-transition/go.sum | 28 ++++++++-------- scripts/tests/tenant-webapp-admission/go.mod | 4 +-- scripts/tests/tenant-webapp-admission/go.sum | 8 ++--- scripts/tests/webapp-compiler/go.mod | 10 +++--- scripts/tests/webapp-compiler/go.sum | 28 ++++++++-------- 18 files changed, 164 insertions(+), 170 deletions(-) diff --git a/go.mod b/go.mod index 08b88e5f82..c632248360 100644 --- a/go.mod +++ b/go.mod @@ -33,9 +33,9 @@ require ( github.com/siderolabs/gen v0.8.8 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect - golang.org/x/net v0.60.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/net v0.62.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.15.0 google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // indirect @@ -95,8 +95,8 @@ require ( go.opentelemetry.io/otel/metric v1.45.0 // indirect go.opentelemetry.io/otel/trace v1.45.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.23.0 // indirect - golang.org/x/term v0.46.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/term v0.47.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.3 // indirect k8s.io/apimachinery v0.37.2 // indirect @@ -138,7 +138,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/crypto v0.57.0 // indirect + golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 // indirect ) replace github.com/kopia/kopia => github.com/project-velero/kopia v0.0.0-20251230033609-d946b1e75197 diff --git a/go.sum b/go.sum index e48f57cea4..3a12895b19 100644 --- a/go.sum +++ b/go.sum @@ -336,27 +336,27 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 h1:zGDaB+/Mus2mScKvZ23k+W9NAUbP56sNM9SptgvWI5A= +golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= -golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= -golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= -golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 h1:dWeMvEJ3JhYgqSCAHUZZJgMUyfniiiCvDc72x5EqJP0= diff --git a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod index a15882b856..4ef14f4d4a 100644 --- a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod @@ -11,7 +11,7 @@ require ( require ( cel.dev/expr v0.25.2 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -26,7 +26,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/cel-go v0.26.0 // indirect + github.com/google/cel-go v0.30.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect @@ -44,7 +44,6 @@ require ( github.com/prometheus/procfs v0.19.2 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.9 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect @@ -58,12 +57,12 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect - golang.org/x/net v0.60.0 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.23.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/term v0.46.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect diff --git a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum index 2e750d4feb..516a1e9e4c 100644 --- a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum @@ -1,7 +1,7 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -39,8 +39,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= -github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo= +github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -95,8 +95,6 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -138,18 +136,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= -golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= -golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/scripts/render-helm-chart/go.mod b/scripts/render-helm-chart/go.mod index a63fed2a36..2114bceb60 100644 --- a/scripts/render-helm-chart/go.mod +++ b/scripts/render-helm-chart/go.mod @@ -84,13 +84,13 @@ require ( go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect diff --git a/scripts/render-helm-chart/go.sum b/scripts/render-helm-chart/go.sum index 232b018ae1..3a0e745246 100644 --- a/scripts/render-helm-chart/go.sum +++ b/scripts/render-helm-chart/go.sum @@ -321,28 +321,28 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 h1:zGDaB+/Mus2mScKvZ23k+W9NAUbP56sNM9SptgvWI5A= +golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= diff --git a/scripts/tests/gateway-receiver-admission/go.mod b/scripts/tests/gateway-receiver-admission/go.mod index f3c1f1e63c..6850ec1f17 100644 --- a/scripts/tests/gateway-receiver-admission/go.mod +++ b/scripts/tests/gateway-receiver-admission/go.mod @@ -26,7 +26,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/cel-go v0.29.0 // indirect + github.com/google/cel-go v0.30.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect @@ -57,12 +57,12 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect - golang.org/x/net v0.58.0 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.41.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect diff --git a/scripts/tests/gateway-receiver-admission/go.sum b/scripts/tests/gateway-receiver-admission/go.sum index 4c381ac80a..516a1e9e4c 100644 --- a/scripts/tests/gateway-receiver-admission/go.sum +++ b/scripts/tests/gateway-receiver-admission/go.sum @@ -39,8 +39,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.29.0 h1:fEG+Ja3YRwNOqnQxTyJwoByAUAvTuxUGiro/jhrm4F4= -github.com/google/cel-go v0.29.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo= +github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -136,18 +136,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/scripts/tests/kyverno-auth-reviews/api/go.mod b/scripts/tests/kyverno-auth-reviews/api/go.mod index 01c9aa43f9..9fb96fe1f0 100644 --- a/scripts/tests/kyverno-auth-reviews/api/go.mod +++ b/scripts/tests/kyverno-auth-reviews/api/go.mod @@ -46,11 +46,11 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect diff --git a/scripts/tests/kyverno-auth-reviews/api/go.sum b/scripts/tests/kyverno-auth-reviews/api/go.sum index 8604b80503..afa54550b8 100644 --- a/scripts/tests/kyverno-auth-reviews/api/go.sum +++ b/scripts/tests/kyverno-auth-reviews/api/go.sum @@ -116,24 +116,24 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= diff --git a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod index 0f95b1cbe8..c2926036cd 100644 --- a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod +++ b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod @@ -11,7 +11,7 @@ require ( require ( cel.dev/expr v0.25.2 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -26,7 +26,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect - github.com/google/cel-go v0.26.0 // indirect + github.com/google/cel-go v0.30.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect @@ -44,7 +44,6 @@ require ( github.com/prometheus/procfs v0.19.2 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.9 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect @@ -58,12 +57,12 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect - golang.org/x/net v0.60.0 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.23.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/term v0.46.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect diff --git a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum index 2e750d4feb..516a1e9e4c 100644 --- a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum +++ b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum @@ -1,7 +1,7 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -39,8 +39,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= -github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo= +github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -95,8 +95,6 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -138,18 +136,18 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= -golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= -golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/scripts/tests/openbao-strategy-transition/go.mod b/scripts/tests/openbao-strategy-transition/go.mod index 96ff3aa1be..89c047c356 100644 --- a/scripts/tests/openbao-strategy-transition/go.mod +++ b/scripts/tests/openbao-strategy-transition/go.mod @@ -65,12 +65,12 @@ require ( github.com/xlab/treeprint v1.2.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect diff --git a/scripts/tests/openbao-strategy-transition/go.sum b/scripts/tests/openbao-strategy-transition/go.sum index 7e51478cc9..ea53e6e62c 100644 --- a/scripts/tests/openbao-strategy-transition/go.sum +++ b/scripts/tests/openbao-strategy-transition/go.sum @@ -158,25 +158,25 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= diff --git a/scripts/tests/tenant-webapp-admission/go.mod b/scripts/tests/tenant-webapp-admission/go.mod index f15ca10bb0..2126f04023 100644 --- a/scripts/tests/tenant-webapp-admission/go.mod +++ b/scripts/tests/tenant-webapp-admission/go.mod @@ -305,9 +305,9 @@ require ( go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.58.0 // indirect + golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/net v0.61.0 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.24.0 // indirect golang.org/x/sys v0.49.0 // indirect diff --git a/scripts/tests/tenant-webapp-admission/go.sum b/scripts/tests/tenant-webapp-admission/go.sum index 237837c0a2..480070bcc1 100644 --- a/scripts/tests/tenant-webapp-admission/go.sum +++ b/scripts/tests/tenant-webapp-admission/go.sum @@ -910,8 +910,8 @@ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1m golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= -golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 h1:zGDaB+/Mus2mScKvZ23k+W9NAUbP56sNM9SptgvWI5A= +golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= @@ -954,8 +954,8 @@ golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= -golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= diff --git a/scripts/tests/webapp-compiler/go.mod b/scripts/tests/webapp-compiler/go.mod index c13b8571a7..d5bf234dfe 100644 --- a/scripts/tests/webapp-compiler/go.mod +++ b/scripts/tests/webapp-compiler/go.mod @@ -62,12 +62,12 @@ require ( go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect - golang.org/x/net v0.58.0 // indirect + golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.41.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260831171406-18b4a7587f8a // indirect diff --git a/scripts/tests/webapp-compiler/go.sum b/scripts/tests/webapp-compiler/go.sum index 2916f8888c..3d4bbeaa0d 100644 --- a/scripts/tests/webapp-compiler/go.sum +++ b/scripts/tests/webapp-compiler/go.sum @@ -149,24 +149,24 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= +golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/genproto/googleapis/api v0.0.0-20260831171406-18b4a7587f8a h1:i3TAXhpKc7TUP1VAPiBBrv45kamjoizCC3rOC0cAbOs= From fef6a32d9fa87bc819a89ec3222269865c2b29ef Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 22:56:57 +0100 Subject: [PATCH 08/19] fix(scan): scope osv-scanner's two expected advisory sets to their modules osv-scanner reports two sets that are not defects to bump away: - The govulncheck negative-control fixture pins Go 1.24.0 on purpose, so its standard-library advisories are the point of the fixture. Its own osv-scanner.toml ignores only the stdlib package there; a dependency advisory added to that module is still reported. - Kyverno v1.19.2 in the offline admission driver carries the three Go records the module's .govulncheck-allow.txt already adjudicates as database-range errors (platform#5154). The module's osv-scanner.toml ignores exactly those IDs with the upstream ranges, and TestAdvisoryAcceptancePinsReviewedRelease now requires the two lists to name the same advisories and keeps fencing the release. osv-scanner reads each file only for the go.mod beside it, so no other module's findings are affected. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- .../osv-scanner.toml | 10 +++++ .../.govulncheck-allow.txt | 3 +- .../tenant-webapp-admission/main_test.go | 38 +++++++++++++++++-- .../tenant-webapp-admission/osv-scanner.toml | 15 ++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 scripts/tests/fixtures/govulncheck-negative-control/osv-scanner.toml create mode 100644 scripts/tests/tenant-webapp-admission/osv-scanner.toml diff --git a/scripts/tests/fixtures/govulncheck-negative-control/osv-scanner.toml b/scripts/tests/fixtures/govulncheck-negative-control/osv-scanner.toml new file mode 100644 index 0000000000..42c353aa8c --- /dev/null +++ b/scripts/tests/fixtures/govulncheck-negative-control/osv-scanner.toml @@ -0,0 +1,10 @@ +# osv-scanner reads this file for the go.mod beside it only. The fixture pins an old Go release +# so that its standard library carries published advisories: the Go vulnerability scan must report +# them, or it has failed (see go.mod and docs/go-vulnerability-scan.md). Those advisories are the +# fixture's purpose, not a defect to fix, and the module is never built or shipped. Only the +# standard library is excluded; any advisory on a dependency added here is still reported. +[[PackageOverrides]] +name = "stdlib" +ecosystem = "Go" +vulnerability.ignore = true +reason = "Negative control: the pinned old Go release must carry standard-library advisories." diff --git a/scripts/tests/tenant-webapp-admission/.govulncheck-allow.txt b/scripts/tests/tenant-webapp-admission/.govulncheck-allow.txt index 267a496943..457b35371d 100644 --- a/scripts/tests/tenant-webapp-admission/.govulncheck-allow.txt +++ b/scripts/tests/tenant-webapp-admission/.govulncheck-allow.txt @@ -9,7 +9,8 @@ # https://github.com/kyverno/kyverno/security/advisories/GHSA-459x-q9hg-4gpq # Go's imported records instead classify all versions/symbols as affected. # This does not accept a vulnerable version, any other advisory or production -# exposure. TestAdvisoryAcceptancePinsReviewedRelease fences dependency changes. +# exposure. TestAdvisoryAcceptancePinsReviewedRelease fences dependency changes +# and requires osv-scanner.toml to ignore exactly these IDs. GO-2026-5594 GO-2026-5337 GO-2025-3615 diff --git a/scripts/tests/tenant-webapp-admission/main_test.go b/scripts/tests/tenant-webapp-admission/main_test.go index 004f4805b0..af8dbc2e6f 100644 --- a/scripts/tests/tenant-webapp-admission/main_test.go +++ b/scripts/tests/tenant-webapp-admission/main_test.go @@ -2,6 +2,9 @@ package main import ( "os" + "regexp" + "slices" + "strings" "testing" policiesv1beta1 "github.com/kyverno/api/api/policies.kyverno.io/v1beta1" @@ -10,14 +13,43 @@ import ( "sigs.k8s.io/yaml" ) +// osvIgnoredID matches an [[IgnoredVulns]] id in osv-scanner.toml. An entry in +// any other form is not matched, so the lists differ and the test fails closed. +var osvIgnoredID = regexp.MustCompile(`(?m)^\s*id\s*=\s*"([^"]+)"\s*$`) + +// readOptional returns a file's contents, or nothing when the file is absent. +func readOptional(t *testing.T, name string) string { + t.Helper() + data, err := os.ReadFile(name) + if os.IsNotExist(err) { + return "" + } else if err != nil { + t.Fatal(err) + } + return string(data) +} + // These acceptances adjudicate overbroad database ranges, not an unpatched // runtime. A dependency update must re-read the upstream ranges before carrying // them forward. Removing the acceptances after database correction is allowed. +// govulncheck and osv-scanner each read their own list, so both must name the +// same advisories. func TestAdvisoryAcceptancePinsReviewedRelease(t *testing.T) { - if _, err := os.Stat(".govulncheck-allow.txt"); os.IsNotExist(err) { + var accepted, ignored []string + for _, line := range strings.Split(readOptional(t, ".govulncheck-allow.txt"), "\n") { + line, _, _ = strings.Cut(line, "#") + accepted = append(accepted, strings.Fields(line)...) + } + for _, match := range osvIgnoredID.FindAllStringSubmatch(readOptional(t, "osv-scanner.toml"), -1) { + ignored = append(ignored, match[1]) + } + slices.Sort(accepted) + slices.Sort(ignored) + if !slices.Equal(accepted, ignored) { + t.Fatalf("osv-scanner.toml ignores %v but .govulncheck-allow.txt accepts %v", ignored, accepted) + } + if len(accepted) == 0 { return - } else if err != nil { - t.Fatal(err) } data, err := os.ReadFile("go.mod") if err != nil { diff --git a/scripts/tests/tenant-webapp-admission/osv-scanner.toml b/scripts/tests/tenant-webapp-admission/osv-scanner.toml new file mode 100644 index 0000000000..3d6527bb5d --- /dev/null +++ b/scripts/tests/tenant-webapp-admission/osv-scanner.toml @@ -0,0 +1,15 @@ +# osv-scanner reads this file for the go.mod beside it only. These are the same three exact-ID +# database-range adjudications for Kyverno v1.19.2 as .govulncheck-allow.txt, which records the +# upstream ranges they rest on; platform#5154 removes both after the Go records are corrected. +# TestAdvisoryAcceptancePinsReviewedRelease keeps the two lists equal and fences the release. +[[IgnoredVulns]] +id = "GO-2026-5594" +reason = "Go record claims every version; upstream GHSA-qr4g-8hrp-c4rw is patched in 1.18.0." + +[[IgnoredVulns]] +id = "GO-2026-5337" +reason = "Go record claims every version; upstream GHSA-cvq5-hhx3-f99p is patched in 1.17.2." + +[[IgnoredVulns]] +id = "GO-2025-3615" +reason = "Go record claims every version; upstream GHSA-459x-q9hg-4gpq affects < v1.13.4." From 6c53b59db7c40a4dcf8f7363abcc76778f424b43 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 22:57:19 +0100 Subject: [PATCH 09/19] fix(images): clear hadolint, Checkov and Trivy findings on hotfix Dockerfiles - Run the govulncheck findings pipelines in the two Kyverno builders under bash with pipefail (hadolint DL4006), so any failed stage fails the build rather than only the last one. - Pin liblz4-dev to bookworm's only build, 1.9.4-1 (DL3008). - Restate each upstream runtime image's own non-root identity with an explicit USER (65532 for the ko-built Kyverno controllers, 65534:65534 for the Coroot server and cluster agent), read from the pinned digests' configs, so runtime behaviour is unchanged (Checkov CKV_DOCKER_3, Trivy DS-0002). - Declare HEALTHCHECK NONE, as the private-mail image already does: Kubernetes never runs an image health check (Trivy DS-0026). Scoped dispositions, each next to what it covers: - MigrationRuntime: `hadolint ignore=DL3059` on its second exec-form check, because the ko-built runtime image has no shell to share one RUN. - CorootNodeAgent: `checkov:skip=CKV_DOCKER_3`; the eBPF node agent keeps the upstream image's root identity by design (the matching Trivy DS-0002 entry is in .trivyignore.yaml). - private-mail-config: `checkov:skip=CKV_DOCKER_4` on the one ADD that fetches a checksum-pinned URL, which COPY cannot do. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- .../kyverno/BackgroundDownstreamCleanup.Dockerfile | 6 ++++++ .../controllers/kyverno/MigrationRuntime.Dockerfile | 6 ++++++ .../controllers/kyverno/ReportsWatchPlan.Dockerfile | 4 ++++ k8s/bases/infrastructure/coroot/CorootCheckpoint.Dockerfile | 6 +++++- .../coroot/CorootClusterAgentCheckpoint.Dockerfile | 4 ++++ k8s/bases/infrastructure/coroot/CorootNodeAgent.Dockerfile | 5 +++++ scripts/private-mail-config/Dockerfile | 2 ++ 7 files changed, 32 insertions(+), 1 deletion(-) diff --git a/k8s/bases/infrastructure/controllers/kyverno/BackgroundDownstreamCleanup.Dockerfile b/k8s/bases/infrastructure/controllers/kyverno/BackgroundDownstreamCleanup.Dockerfile index 158fa1f73b..f7be300ccf 100644 --- a/k8s/bases/infrastructure/controllers/kyverno/BackgroundDownstreamCleanup.Dockerfile +++ b/k8s/bases/infrastructure/controllers/kyverno/BackgroundDownstreamCleanup.Dockerfile @@ -23,6 +23,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath \ && if grep -Fq 'golang.org/x/crypto/openpgp' /tmp/symbols; then \ echo 'Unmaintained OpenPGP package compiled into the controller'; exit 1; \ else test "$?" -eq 1; fi +# The findings pipeline below must fail when any of its stages fails, not only the last one. +SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Only these exact module records are accepted. The three Kyverno v1.19.2 records have the same # adjudication and removal as scripts/tests/tenant-webapp-admission/.govulncheck-allow.txt # (platform#5154). GO-2026-5932 names x/crypto/openpgp and has no fixed version; the symbol check @@ -41,4 +43,8 @@ RUN GOBIN=/usr/local/bin go install golang.org/x/vuln/cmd/govulncheck@v1.8.0 \ FROM reg.kyverno.io/kyverno/background-controller:v1.19.2@sha256:c46e124f21ef8a6bb4b48de57106808b1007edcd5ddd4ff7df5d23b9efa60ce2 COPY --from=builder --chmod=0755 /out/background-controller /ko-app/background-controller +# Restate the upstream image's own non-root identity; its config User is 65532. +USER 65532 +# Kubernetes never runs an image health check, so this image declares none. +HEALTHCHECK NONE RUN ["/ko-app/background-controller", "--help"] diff --git a/k8s/bases/infrastructure/controllers/kyverno/MigrationRuntime.Dockerfile b/k8s/bases/infrastructure/controllers/kyverno/MigrationRuntime.Dockerfile index ec169fc6db..b9a6bfb125 100644 --- a/k8s/bases/infrastructure/controllers/kyverno/MigrationRuntime.Dockerfile +++ b/k8s/bases/infrastructure/controllers/kyverno/MigrationRuntime.Dockerfile @@ -26,6 +26,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath \ && if /out/kubectl-kyverno apply --help > /tmp/unsupported-command 2>&1; then \ echo 'Unsupported CLI command exposed by migration runtime'; exit 1; \ else test "$?" -eq 1 && grep -F 'unknown command "apply"' /tmp/unsupported-command; fi +# The findings pipeline below must fail when any of its stages fails, not only the last one. +SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Only these exact whole-module records for Kyverno v1.19.2 are accepted, with the same # adjudication and removal as scripts/tests/tenant-webapp-admission/.govulncheck-allow.txt # (platform#5154). The list must match exactly: another advisory fails the build, and so does @@ -44,6 +46,10 @@ FROM reg.kyverno.io/kyverno/kyverno-cli:v1.19.2@sha256:322ec5044d600930cb3af0e56 COPY --from=builder --chmod=0755 /out/kubectl-kyverno /ko-app/kubectl-kyverno # Match the released migration Job's explicit execution identity. USER 65534:65534 +# Kubernetes never runs an image health check, so this image declares none. +HEALTHCHECK NONE ENTRYPOINT ["/ko-app/kubectl-kyverno"] RUN ["/ko-app/kubectl-kyverno", "--version"] +# The runtime image has no shell, so these exec-form checks cannot share one RUN. +# hadolint ignore=DL3059 RUN ["/ko-app/kubectl-kyverno", "migrate", "--help"] diff --git a/k8s/bases/infrastructure/controllers/kyverno/ReportsWatchPlan.Dockerfile b/k8s/bases/infrastructure/controllers/kyverno/ReportsWatchPlan.Dockerfile index 065de29090..1f8fe785f2 100644 --- a/k8s/bases/infrastructure/controllers/kyverno/ReportsWatchPlan.Dockerfile +++ b/k8s/bases/infrastructure/controllers/kyverno/ReportsWatchPlan.Dockerfile @@ -15,4 +15,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath \ FROM reg.kyverno.io/kyverno/reports-controller:v1.19.2@sha256:5fb6d3e738050dbce84f60ecabaede7665fbe0578d07c1e8c912443b035e79c3 COPY --from=builder --chmod=0755 /out/reports-controller /ko-app/reports-controller +# Restate the upstream image's own non-root identity; its config User is 65532. +USER 65532 +# Kubernetes never runs an image health check, so this image declares none. +HEALTHCHECK NONE RUN ["/ko-app/reports-controller", "--help"] diff --git a/k8s/bases/infrastructure/coroot/CorootCheckpoint.Dockerfile b/k8s/bases/infrastructure/coroot/CorootCheckpoint.Dockerfile index aa02127cdd..d5a834eac2 100644 --- a/k8s/bases/infrastructure/coroot/CorootCheckpoint.Dockerfile +++ b/k8s/bases/infrastructure/coroot/CorootCheckpoint.Dockerfile @@ -6,7 +6,7 @@ COPY front/ ./ RUN npm run build-prod FROM docker.io/library/golang:1.27.3-bookworm@sha256:fea12ba16096adec0f699ce1c4da2154d320450e56c1f63671f95d2a3551c77b AS builder -RUN apt-get update && apt-get install --yes --no-install-recommends liblz4-dev && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install --yes --no-install-recommends liblz4-dev=1.9.4-1 && rm -rf /var/lib/apt/lists/* WORKDIR /src COPY go.mod go.sum ./ RUN go mod download @@ -18,4 +18,8 @@ RUN go build -mod=readonly -ldflags "-X main.version=$VERSION" -o /out/coroot . FROM ghcr.io/coroot/coroot:1.29.1@sha256:7f5aa2982ff789f95d8326b35cafad3d127bb44669eed94a90a6baa563c342af COPY --from=builder --chmod=0755 /out/coroot /usr/bin/coroot +# Restate the upstream image's own non-root identity; its config User is 65534:65534. +USER 65534:65534 +# Kubernetes never runs an image health check, so this image declares none. +HEALTHCHECK NONE RUN ["/usr/bin/coroot", "--help"] diff --git a/k8s/bases/infrastructure/coroot/CorootClusterAgentCheckpoint.Dockerfile b/k8s/bases/infrastructure/coroot/CorootClusterAgentCheckpoint.Dockerfile index 5f455d0b55..4df8b5be7c 100644 --- a/k8s/bases/infrastructure/coroot/CorootClusterAgentCheckpoint.Dockerfile +++ b/k8s/bases/infrastructure/coroot/CorootClusterAgentCheckpoint.Dockerfile @@ -9,4 +9,8 @@ RUN go build -mod=readonly -ldflags "-X main.version=$VERSION" -o /out/coroot-cl FROM ghcr.io/coroot/coroot-cluster-agent:1.11.8@sha256:a5bdb68c23fac361de19531cac8e9a0fcef4692ea10edac3d2ef9e6769414de7 COPY --from=builder --chmod=0755 /out/coroot-cluster-agent /usr/bin/coroot-cluster-agent +# Restate the upstream image's own non-root identity; its config User is 65534:65534. +USER 65534:65534 +# Kubernetes never runs an image health check, so this image declares none. +HEALTHCHECK NONE RUN ["/usr/bin/coroot-cluster-agent", "--help"] diff --git a/k8s/bases/infrastructure/coroot/CorootNodeAgent.Dockerfile b/k8s/bases/infrastructure/coroot/CorootNodeAgent.Dockerfile index 28da046b13..76c9a18dcf 100644 --- a/k8s/bases/infrastructure/coroot/CorootNodeAgent.Dockerfile +++ b/k8s/bases/infrastructure/coroot/CorootNodeAgent.Dockerfile @@ -1,3 +1,8 @@ FROM ghcr.io/coroot/coroot-node-agent:1.38.1@sha256:a1488ca3e541703fe4b4cdefeff2dae992cc9f45affd18f9f41e5b12a8016cae COPY --chmod=0755 coroot-node-agent /usr/bin/coroot-node-agent +# The node agent loads eBPF programs and reads every host process, so it keeps the upstream image's +# root identity (its config User is empty) instead of naming a non-root USER here. +# checkov:skip=CKV_DOCKER_3:The eBPF node agent keeps the upstream image's root identity by design. +# Kubernetes never runs an image health check, so this image declares none. +HEALTHCHECK NONE diff --git a/scripts/private-mail-config/Dockerfile b/scripts/private-mail-config/Dockerfile index 433d387d07..6153330304 100644 --- a/scripts/private-mail-config/Dockerfile +++ b/scripts/private-mail-config/Dockerfile @@ -10,6 +10,8 @@ RUN mkdir -p /usr/local/share/stalwart /usr/local/share/licenses/stalwart-webui chmod 0755 /usr/local/share /usr/local/share/stalwart /usr/local/share/licenses /usr/local/share/licenses/stalwart-webui COPY --from=cli /usr/local/bin/stalwart-cli /usr/local/bin/stalwart-cli COPY --from=client-proxy --chmod=0755 /usr/bin/caddy /usr/local/bin/caddy +# COPY cannot fetch a URL. ADD fetches this one file and BuildKit verifies its pinned digest. +# checkov:skip=CKV_DOCKER_4:Remote fetch of one checksum-pinned file, which COPY cannot do. ADD --chmod=0444 --checksum=sha256:cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 https://raw.githubusercontent.com/caddyserver/caddy/v2.11.7/LICENSE /usr/local/share/licenses/caddy/LICENSE COPY --chmod=0755 private-mail-bootstrap /usr/local/bin/private-mail-bootstrap COPY --chmod=0444 webui.zip /usr/local/share/stalwart/webui.zip From d8b7520a83aaa6af4a94ddf30c7fc95dfbdcf056 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 3 Nov 2026 22:57:46 +0100 Subject: [PATCH 10/19] fix(k8s): clear Checkov and Trivy findings on manifests Fixes: - recover-vault-snapshot becomes a JSON patch with its target in the Hetzner kustomization, following keep-vault-snapshot-on-baseline- workers: a partial Job is scanned as a workload with no containers (Checkov CKV_K8S_23/29/31/38/40, Trivy KSV-0118). The rendered Hetzner infrastructure layer is byte-identical. - The coroot-dns-probe admission fixture states a restricted pod baseline (15 Trivy findings); the ndots mutation it exercises reads only labels, namespace and dnsConfig, and still applies. Path- or resource-scoped dispositions, each with its reason: - KSV-0040 (579, LOW): skipped like KSV-0039. Trivy evaluates the namespace ResourceQuota check per file, so every non-ResourceQuota file fails; the repository's one ResourceQuota passes. - private-mail: uid/gid 2000 is the stalwart identity baked into the pinned Stalwart digest (KSV-0020/0021, CKV_K8S_40), NET_BIND_SERVICE is needed for the binary's file capability (KSV-0022, CKV_K8S_25), recovery mode takes its admin from an env var (CKV_K8S_35), and the two overlay fragments get their image digest and pod securityContext from the kustomization (KSV-0013/0118, CKV_K8S_14/15/29/43). Resource-scoped checkov.io/skipN annotations are numbered across the base and both patches so the merged StatefulSet keeps every reason; only that object's metadata changes in the rendered output. - KSV-0036 on the three pods that disable automounting and project a token only into the container that authenticates. - KSV-0037 on kube-system's own LimitRange, now under bootstrap. - KSV-0048/0049/0053 on five namespaced single-rule Roles (Longhorn Kyverno retrofits, the orphan-alert state record, the owner-only World at Ruin trial). - KSV-01010/0109 on the Homepage layout and the generated security-progress ConfigMap. - DS-0002 on the root eBPF Coroot node agent. Guards: a new test-trivyignore-private-mail-identity-boundary.sh asserts the identity per container, the measured digest and the bootstrap USER, and keeps an identical probe reported (wired into CI). The RBAC test asserts the five new roles' shape and the trial role's sole owner binding; the ConfigMap test pins both new files' matched key sets and clears absolute file paths from its opaque-value premise; the reciprocal path lists in the vendored, vault-config and vault-backup tests name the new reviewed pairs. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yaml | 13 ++ .trivyignore.yaml | 188 +++++++++++++++++- .../patches/serve-after-initialization.yaml | 7 + .../patches/add-client-proxy.yaml | 7 + k8s/bases/apps/private-mail/stateful-set.yaml | 6 + .../hetzner/infrastructure/kustomization.yaml | 6 + .../patches/recover-vault-snapshot.yaml | 20 +- .../tests/fixtures/coroot-dns-probe/pod.yaml | 21 ++ ...-trivyignore-configmap-content-boundary.sh | 47 ++++- ...t-trivyignore-first-party-rbac-boundary.sh | 67 +++++++ ...vyignore-private-mail-identity-boundary.sh | 129 ++++++++++++ ...vyignore-vault-backup-identity-boundary.sh | 4 + ...vyignore-vault-config-identity-boundary.sh | 7 + ...-trivyignore-vendored-operator-boundary.sh | 14 ++ 14 files changed, 515 insertions(+), 21 deletions(-) create mode 100755 scripts/tests/test-trivyignore-private-mail-identity-boundary.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6a0a734606..8a53732fe7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -458,6 +458,9 @@ jobs: # The vault-backup KSV-0020 disposition rests on the same per-container identity, # and its guard also asserts that KSV-0021 stays UNdispositioned there. - 'scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh' + # The private-mail KSV-0020/KSV-0021 disposition rests on Stalwart's image-baked + # identity per container, so the entry and its premises test move together. + - 'scripts/tests/test-trivyignore-private-mail-identity-boundary.sh' # The first-party RBAC dispositions and the roles they describe are one # contract: the disposition is only valid while the role stays bound and # scoped the way it was reviewed. The roles themselves are already covered @@ -2276,6 +2279,16 @@ jobs: shellcheck scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh bash scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh + - name: 🧱 Validate private-mail identity disposition boundary + if: needs.changes.outputs.k8s == 'true' + # Every private-mail container runs at uid/gid 2000, the stalwart identity the pinned + # Stalwart digest defines and the bootstrap image restates. The path-scoped entry would + # also hide a container added later at another low identity, so the guard asserts the + # identity per container and the measured digest, and keeps an identical probe reported. + run: | + shellcheck scripts/tests/test-trivyignore-private-mail-identity-boundary.sh + bash scripts/tests/test-trivyignore-private-mail-identity-boundary.sh + - name: 🧱 Validate first-party RBAC disposition premises if: needs.changes.outputs.k8s == 'true' # The four first-party RBAC dispositions rest on facts trivy cannot see: the tenant diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 474faa422a..3eafd7145a 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -29,6 +29,24 @@ misconfigurations: LimitRange is namespace-scoped but this check evaluates each file independently, including resources that cannot carry a LimitRange and the LimitRange manifests themselves. + # KSV-0040 "resource quota usage" — 579 findings on this tree, all LOW, first reported when + # MegaLinter 10.1.0 (trivy 0.74.0) began scanning it with the current checks bundle. + # + # The same per-FILE evaluation as KSV-0039, for a namespace's ResourceQuota instead of its + # LimitRange. The check's input selector names ResourceQuota, but `trivy fs` applies it to every + # Kubernetes file, so each file that is not a ResourceQuota fails it — the firing set is + # 47 cilium-network-policy.yaml, 45 helm-release.yaml, 32 helm-repository.yaml, + # 24 pod-disruption-budget.yaml, 15 oci-repository.yaml and so on. The one ResourceQuota on this + # tree, k8s/providers/hetzner/apps/world-at-ruin/resource-quota.yaml, sets all four hard fields and + # passes. No per-file edit can satisfy the rest, so this is not a deferral either. + # + # ⚠️ The cost is the same as KSV-0039's: a ResourceQuota added later without the four hard fields is + # not reported here. + - id: KSV-0040 + statement: >- + ResourceQuota is namespace-scoped but this check evaluates each file independently, so every + resource that is not a ResourceQuota fails it; the repository's only ResourceQuota passes. + # KSV-0037 "user resources should not be placed in kube-system namespace" — 47 of trivy's 157 # findings on this tree (30%), all MEDIUM. # @@ -68,14 +86,20 @@ misconfigurations: # input-derived namespace expression — # `${tenantNamespace.metadata.name}` in Tenant or `${schema.spec.name}` in WebApp. Their only # kube-system references are RBAC grants naming the shared Gateway, not workload placement. + # + # The kube-system LimitRange has since moved to k8s/bases/bootstrap/limit-ranges/, where the + # bootstrap layer applies it before anything else. It is listed by its exact path rather than by + # widening a glob over the bootstrap tree, which also holds the shared Flux variables. - id: KSV-0037 paths: - k8s/bases/infrastructure/** - k8s/providers/*/infrastructure/** + - k8s/bases/bootstrap/limit-ranges/kube-system.yaml statement: >- These are cluster infrastructure components that belong in kube-system by construction — CNI, DNS, CSI, cloud-controller-manager and their namespace-local VerticalPodAutoscalers, - PodDisruptionBudgets and Gateway material. Application trees remain covered by this check. + PodDisruptionBudgets and Gateway material, plus kube-system's own default LimitRange. + Application trees remain covered by this check. # KSV-0109 matches identifier text rather than secret material at both sites below. Keep the # dispositions separate so each path carries the reason that was checked against its content. @@ -665,3 +689,165 @@ misconfigurations: primary gid, so the split costs no access. Because trivy scopes by path rather than by resource, scripts/tests/test-trivyignore-vault-config-identity-boundary.sh asserts that per container so this entry cannot silently cover a future low-GID container. + + # The private-mail StatefulSet runs Stalwart at the identity its own image defines, measured + # against the pinned DIGEST ghcr.io/stalwartlabs/stalwart@sha256:c429b311c93d…885836c rather than + # the tag: the image config's User is `stalwart`, and its layers carry `stalwart:x:2000:2000` in + # /etc/passwd and `stalwart:x:2000` in /etc/group. The platform bootstrap image is built FROM that + # same digest and ends with `USER 2000:2000` (scripts/private-mail-config/Dockerfile), so every + # container here — Stalwart, the configure init container and the Caddy client-proxy — restates + # the identity its image runs as. The mail store on the data volume is owned by it as well, so a + # different UID would also lose write access to existing data. + # + # The two patches are fragments of the same StatefulSet: serve-after-initialization replaces its + # pod spec, and add-client-proxy adds the client-proxy container, which inherits that pod's + # securityContext. Both are scanned on their own, so each reports the identity it carries or + # inherits. + # + # ⚠️ Path-scoped like the openbao entries above, so a container added to one of these files at + # another low identity would be suppressed too. + # scripts/tests/test-trivyignore-private-mail-identity-boundary.sh asserts per container that + # nothing but 2000:2000 runs here and that the images are still the measured digest. + - id: KSV-0020 + paths: + - k8s/bases/apps/private-mail/stateful-set.yaml + - k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml + - k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml + statement: >- + UID 2000 is the stalwart user the digest-pinned Stalwart image defines and runs as; the + bootstrap image is built from that digest with USER 2000:2000, and the mail store on the data + volume is owned by that identity. The boundary test asserts every container in these files + runs at it and that the measured image digest is unchanged. + - id: KSV-0021 + paths: + - k8s/bases/apps/private-mail/stateful-set.yaml + - k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml + - k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml + statement: >- + GID 2000 is the stalwart group the same digest-pinned image defines (/etc/group + stalwart:x:2000) and that the UID entry above runs with; fsGroup 2000 owns the mail store. + The same boundary test asserts it per container. + + # Stalwart's binary carries an effective NET_BIND_SERVICE file capability, and a binary with an + # effective file capability cannot be executed while that capability is outside the container's + # bounding set, even when it binds only an unprivileged port (recovery mode serves 8080). Both + # containers that exec it drop every capability and add back only this one. The Caddy + # client-proxy, which binds only 8081, had the capability removed from its binary at image build + # (`setcap -r` in scripts/private-mail-config/Dockerfile) and adds nothing. + - id: KSV-0022 + paths: + - k8s/bases/apps/private-mail/stateful-set.yaml + - k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml + statement: >- + The Stalwart binary's effective file capability requires NET_BIND_SERVICE in the bounding + set for it to execute at all; each container drops every other capability and runs as the + non-root image identity. + + # KSV-0013 "image tag" on two StatefulSet patch fragments. Each names only the repository of the + # platform bootstrap image: private-mail-administration/kustomization.yaml pins it by digest with + # its images transformer, and private-mail-proxy-identity copies that same reference onto the + # client-proxy container with a replacement, so every rendered container is digest-pinned. Writing + # the digest into the fragments as well would leave two copies to keep in step. + - id: KSV-0013 + paths: + - k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml + - k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml + statement: >- + The fragments name only the image repository; the kustomization's images transformer and + replacement pin every rendered container to the signed bootstrap image digest. + + # KSV-0118 "default security context" on the add-client-proxy fragment. The fragment adds one + # container and carries that container's full securityContext; the pod securityContext + # (runAsNonRoot, 2000:2000, fsGroup and RuntimeDefault seccomp) comes from the + # serve-after-initialization pod spec this overlay builds on. + - id: KSV-0118 + paths: + - k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml + statement: >- + This patch adds a hardened container to a StatefulSet whose pod securityContext is set by + serve-after-initialization; the rendered pod does not use the default security context. + + # KSV-0036 reads automountServiceAccountToken from the WORKLOAD's own spec — a Deployment, Job or + # CronJob spec, which has no such field — and otherwise fires on any container that mounts the + # conventional token path. Each of these pods sets automountServiceAccountToken: false and instead + # projects a short-lived token, through an explicit volume, into the one container that + # authenticates with it: the vault-backup snapshot container logs in to OpenBao through its + # Kubernetes auth method (the checkov.io/skip1 CKV_K8S_38 annotation on both workloads records + # the same reason), and the security-progress collector reads its scan results through the API. + - id: KSV-0036 + paths: + - k8s/bases/infrastructure/vault-backup/job.yaml + - k8s/bases/infrastructure/vault-backup/cron-job.yaml + - k8s/bases/infrastructure/coroot/components/security-progress/deployment.yaml + statement: >- + The pods disable token automounting and project a short-lived token only into the container + that authenticates with it; the check cannot see the pod-level setting. + + # The three Longhorn retrofit roles have the same shape as the umami Kyverno role above: a + # namespaced Role in longhorn-system, bound to the Kyverno background controller, whose one rule is + # pinned by resourceNames to the workloads its mutate-existing policy names, with verbs get, patch + # and update. No create, no delete, and no other Deployment or DaemonSet in the namespace. + - id: KSV-0048 + paths: + - k8s/providers/hetzner/infrastructure/controllers/longhorn/role-kyverno-mutate-csi-sidecars.yaml + - k8s/providers/hetzner/infrastructure/controllers/longhorn/role-kyverno-mutate-longhorn-csi-plugin.yaml + - k8s/providers/hetzner/infrastructure/controllers/longhorn/role-kyverno-mutate-longhorn-engine-image.yaml + statement: >- + Namespaced longhorn-system Roles for the Kyverno background controller, each pinned by + resourceNames to the workloads one mutate-existing policy retrofits, with get, patch and update + only; they can neither create nor delete a workload nor reach any other. + + # The prune-protected orphan alert's only write: get and patch on its own first-seen record, one + # ConfigMap by resourceNames. `create` is deliberately absent because Flux creates the object. + - id: KSV-0049 + paths: + - k8s/bases/infrastructure/controllers/prune-protected-orphan-alert/role.yaml + statement: >- + A namespaced Role whose single rule grants get and patch on exactly one ConfigMap by + resourceNames — the detector's own state record — with no create, delete or wider reach. + + # The World at Ruin zone trial's operator role: pods/exec and pods/portforward create in the + # world-at-ruin namespace, bound to the owner's own OIDC user and nothing else. The role's header + # records the decision and its retirement condition: it goes before player accounts, authoritative + # persistence or production zone credentials exist. The namespace has no Secret API or workload + # write grant for this user. + - id: KSV-0053 + paths: + - k8s/providers/hetzner/apps/world-at-ruin/role-operator.yaml + statement: >- + An owner-only, namespaced exception for the private zone trial, bound to the owner's OIDC user + and retired before the zone holds player accounts, persistent state or production credentials. + + # KSV-01010 matches `key:`, `email:`, SHA1 and similar TEXT anywhere in a ConfigMap. In the + # homepage layout it matched the `Email:` dashboard group heading; in the generated + # security-progress ConfigMap it matched jq source that builds `{key, value}` entries. Neither + # holds credential material. The security-progress match for KSV-0109 is the prose comment + # "never a clean pass" in the embedded scripts/check-kubescape-result-coverage.sh. Both files' + # matched sets are pinned by scripts/tests/test-trivyignore-configmap-content-boundary.sh. + - id: KSV-01010 + paths: + - k8s/bases/apps/homepage/config-map.yaml + statement: >- + The match is the Email service group heading in the Homepage layout, not a credential. + - id: KSV-01010 + paths: + - k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml + statement: >- + The ConfigMap is generated from reviewed scripts and dashboards; Trivy matches jq identifiers + named key, not credential values. + - id: KSV-0109 + paths: + - k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml + statement: >- + The match is the word pass in an embedded script comment, not secret material. + + # DS-0002 "no non-root USER" on the Coroot node-agent hotfix image. The node agent loads eBPF + # programs and reads every host process, so it keeps the upstream image's root identity (the + # upstream config User is empty) rather than naming a non-root USER. The Dockerfile carries the + # matching resource-scoped checkov skip for CKV_DOCKER_3. The other hotfix images restate their + # upstream non-root identity with an explicit USER. + - id: DS-0002 + paths: + - k8s/bases/infrastructure/coroot/CorootNodeAgent.Dockerfile + statement: >- + The eBPF node agent runs as root by design, inheriting the upstream image's identity. diff --git a/k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml b/k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml index c9c945f3b3..8d328f0707 100644 --- a/k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml +++ b/k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml @@ -3,6 +3,13 @@ kind: StatefulSet metadata: name: private-mail namespace: private-mail + # Checkov scans this fragment on its own; these keys follow the base StatefulSet's skip1-skip3. + annotations: + checkov.io/skip4: "CKV_K8S_14=the kustomization's images transformer pins this repository-only reference to the signed bootstrap digest" + checkov.io/skip5: "CKV_K8S_43=the kustomization's images transformer pins this repository-only reference to the signed bootstrap digest" + checkov.io/skip6: "CKV_K8S_15=IfNotPresent is safe because the rendered image is digest-pinned by the kustomization's images transformer" + checkov.io/skip7: "CKV_K8S_25=NET_BIND_SERVICE must stay in the bounding set for the Stalwart binary's effective file capability to execute; every other capability is dropped" + checkov.io/skip8: "CKV_K8S_40=UID 2000 is the stalwart identity the bootstrap image's Stalwart base defines and runs as, which also owns the mail store" spec: template: spec: diff --git a/k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml b/k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml index cdad550584..609be69dfa 100644 --- a/k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml +++ b/k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml @@ -3,6 +3,13 @@ kind: StatefulSet metadata: name: private-mail namespace: private-mail + # Checkov scans this fragment on its own; these keys follow serve-after-initialization's skip8. + annotations: + checkov.io/skip9: "CKV_K8S_14=the replacement in this kustomization copies the digest-pinned bootstrap image onto this container" + checkov.io/skip10: "CKV_K8S_43=the replacement in this kustomization copies the digest-pinned bootstrap image onto this container" + checkov.io/skip11: "CKV_K8S_15=IfNotPresent is safe because the rendered image is digest-pinned by this kustomization's replacement" + checkov.io/skip12: "CKV_K8S_29=the pod securityContext comes from serve-after-initialization, which this overlay builds on" + checkov.io/skip13: "CKV_K8S_40=the container runs at the pod's UID 2000, the stalwart identity its bootstrap image runs as" spec: template: spec: diff --git a/k8s/bases/apps/private-mail/stateful-set.yaml b/k8s/bases/apps/private-mail/stateful-set.yaml index 38cf10ce30..1e7831dbb4 100644 --- a/k8s/bases/apps/private-mail/stateful-set.yaml +++ b/k8s/bases/apps/private-mail/stateful-set.yaml @@ -7,6 +7,12 @@ metadata: # A single management-only preparation instance owns its local datastore. # Public mail activation must separately establish its availability model. platform.devantler.tech/replica-floor: exempt + # Resource-scoped Checkov dispositions. The overlay patches carry their own, numbered after these + # so the merged object keeps every reason. The Trivy counterparts are in .trivyignore.yaml. + annotations: + checkov.io/skip1: "CKV_K8S_40=UID 2000 is the stalwart user baked into the digest-pinned Stalwart image (stalwart:x:2000:2000), which also owns the mail store" + checkov.io/skip2: "CKV_K8S_25=NET_BIND_SERVICE must stay in the bounding set for the Stalwart binary's effective file capability to execute; every other capability is dropped" + checkov.io/skip3: "CKV_K8S_35=Stalwart's recovery mode takes its administrator credential from the STALWART_RECOVERY_ADMIN environment variable" spec: serviceName: private-mail replicas: 1 diff --git a/k8s/providers/hetzner/infrastructure/kustomization.yaml b/k8s/providers/hetzner/infrastructure/kustomization.yaml index cd1452d43c..8ad0c618d5 100644 --- a/k8s/providers/hetzner/infrastructure/kustomization.yaml +++ b/k8s/providers/hetzner/infrastructure/kustomization.yaml @@ -276,6 +276,12 @@ patches: # vault-snapshots, so no explicit target is needed. - path: patches/store-vault-snapshots-on-hcloud.yaml - path: patches/recover-vault-snapshot.yaml + target: + group: batch + version: v1 + kind: Job + name: vault-snapshot-init + namespace: openbao # Prod-only: the vault-snapshot CronJob and Job mount that hcloud volume, so # both require a static baseline worker (#3287). JSON patches don't # self-identify, so each names its target. diff --git a/k8s/providers/hetzner/infrastructure/patches/recover-vault-snapshot.yaml b/k8s/providers/hetzner/infrastructure/patches/recover-vault-snapshot.yaml index 6cfe19b629..04c7152721 100644 --- a/k8s/providers/hetzner/infrastructure/patches/recover-vault-snapshot.yaml +++ b/k8s/providers/hetzner/infrastructure/patches/recover-vault-snapshot.yaml @@ -2,13 +2,13 @@ # A pod-template change makes Flux replace this immutable, force-enabled Job. # Retaining the completed Job (no TTL) prevents repeated volume attachments; # the daily CronJob remains the normal backup schedule. -apiVersion: batch/v1 -kind: Job -metadata: - name: vault-snapshot-init - namespace: openbao -spec: - template: - metadata: - annotations: - backup.devantler.tech/recovery-id: "2026-09-08-csi-metadata" +# +# JSON patch (not strategic merge), with the target in kustomization.yaml: a +# partial Job manifest is scanned by Checkov and Trivy as if it were a workload +# with no containers or security context, although the rendered Job is +# unchanged in those respects. An operation list is not a workload manifest. +# The base pod template has no annotations, so `add` creates them. +- op: add + path: /spec/template/metadata/annotations + value: + backup.devantler.tech/recovery-id: "2026-09-08-csi-metadata" diff --git a/scripts/tests/fixtures/coroot-dns-probe/pod.yaml b/scripts/tests/fixtures/coroot-dns-probe/pod.yaml index 88a391e568..b19334f577 100644 --- a/scripts/tests/fixtures/coroot-dns-probe/pod.yaml +++ b/scripts/tests/fixtures/coroot-dns-probe/pod.yaml @@ -7,6 +7,27 @@ metadata: app.kubernetes.io/part-of: coroot app.kubernetes.io/managed-by: coroot-operator spec: + # The ndots policy reads only the labels, namespace and dnsConfig. The rest is a restricted pod + # baseline, so the fixture is not itself a scanner finding. + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 65535 + runAsGroup: 65535 + seccompProfile: + type: RuntimeDefault containers: - name: fixture image: registry.k8s.io/pause:3.10 + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 10m + memory: 16Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: [ALL] diff --git a/scripts/tests/test-trivyignore-configmap-content-boundary.sh b/scripts/tests/test-trivyignore-configmap-content-boundary.sh index 2546113255..f03228d301 100755 --- a/scripts/tests/test-trivyignore-configmap-content-boundary.sh +++ b/scripts/tests/test-trivyignore-configmap-content-boundary.sh @@ -5,7 +5,7 @@ # # WHY THIS EXISTS # KSV-01010 "ConfigMap with sensitive content" matches key and value TEXT, so it cannot tell a -# public ACME contact address from a credential. Three ConfigMaps are dispositioned (#3240): +# public ACME contact address from a credential. Five ConfigMaps are dispositioned (#3240): # # k8s/clusters/{prod,local}/bootstrap/config-map.yaml — matched on admin_email, the ACME # registration contact the cluster issuer @@ -13,6 +13,10 @@ # k8s/bases/apps/actual-budget/config-map.yaml — matched on the identifier names in an # embedded script that READS credentials # from an ESO-mounted Secret. +# k8s/bases/apps/homepage/config-map.yaml — matched on the `Email:` service group +# heading in the dashboard layout. +# .../coroot/components/security-progress/config-map.yaml — generated from reviewed scripts; +# matched on jq identifiers named key. # # The premise is that the EXCEPTION IS THE PATH, not the content. If the scoping ever stopped # deciding — a glob widened to k8s/**, the id skipped outright — then a genuinely secret-bearing @@ -54,6 +58,12 @@ readonly ISSUED_PREFIX_RE='^[[:space:]]*[A-Za-z0-9_.-]+:[[:space:]]*"?(gh[pousr] # would have removed real coverage. The "standard base64 containing a slash" fixture pins that. readonly IMAGE_REF_VALUE_RE='^[[:space:]]*[A-Za-z0-9_.-]+:[[:space:]]*"?[a-z0-9-]+(\.[a-z0-9-]+)+(/[a-z0-9._-]+)+"?[[:space:]]*$' +# An absolute file path (`/var/run/secrets/kubernetes.io/serviceaccount/token`) is the same kind +# of false match, and is excluded the same way: the WHOLE value is lowercase path segments that +# start at `/`. Standard base64 can also start with `/`, but carries uppercase, so the +# "standard base64 starting with a slash" fixture below stays detected. +readonly FILE_PATH_VALUE_RE='^[[:space:]]*[A-Za-z0-9_.-]+:[[:space:]]*"?(/[a-z0-9._-]+)+"?[[:space:]]*$' + # Credential-shaped lines in a file, minus image/registry references. Defined once so the file loop # and the self-test exercise the SAME composition — a self-test against a differently-composed # detector proves nothing about the one that runs. @@ -84,7 +94,7 @@ opaque_value_hits() { # This stage filters an in-memory string, so there is nothing left to fail on a read and exit 1 # means every candidate was an image reference — a legitimate empty result. - printf '%s\n' "$candidates" | grep -vE "$IMAGE_REF_VALUE_RE" || true + printf '%s\n' "$candidates" | grep -vE -e "$IMAGE_REF_VALUE_RE" -e "$FILE_PATH_VALUE_RE" || true } fail() { @@ -97,6 +107,8 @@ fail() { command -v yq >/dev/null 2>&1 || fail "yq is required to check the KSV-01010 disposition boundary" readonly EXPECTED_PATHS="k8s/bases/apps/actual-budget/config-map.yaml +k8s/bases/apps/homepage/config-map.yaml +k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml k8s/clusters/local/bootstrap/config-map.yaml k8s/clusters/prod/bootstrap/config-map.yaml" @@ -109,10 +121,10 @@ instead, delete this test with them" while IFS= read -r n; do [ "$n" -gt 0 ] || fail \ "a $CHECK_ID entry has no paths key, so the check is skipped repository-wide rather than \ -scoped to the three reviewed ConfigMaps" +scoped to the five reviewed ConfigMaps" done < <(yq ".misconfigurations[] | select(.id == \"$CHECK_ID\") | (.paths // []) | length" "$IGNOREFILE") -# The scoped set must be exactly the three reviewed files. A widened glob (k8s/**) or a new path +# The scoped set must be exactly the five reviewed files. A widened glob (k8s/**) or a new path # added without review both land here. actual_paths="$(yq -r ".misconfigurations[] | select(.id == \"$CHECK_ID\") | .paths[]" "$IGNOREFILE" | sort)" if [ "$actual_paths" != "$EXPECTED_PATHS" ]; then @@ -124,7 +136,7 @@ actual: $actual_paths" fi -echo "PASS(structural): $CHECK_ID is path-scoped to exactly the 3 reviewed ConfigMaps." +echo "PASS(structural): $CHECK_ID is path-scoped to exactly the 5 reviewed ConfigMaps." # --- Layer 1b: content premises. Path scoping decides WHICH files are excepted; it says nothing # --- about what those files may later contain. A credential added to one of them would be @@ -252,6 +264,11 @@ check_fixture yes no "hostname-prefixed credential (slack webhook)" \ # credential containing `/` must still be caught. check_fixture yes no "standard base64 containing a slash" 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVph/2NkZWZnaGlq=' +# The file-path exclusion must clear a projected token path and still catch base64 that merely +# starts with `/`, which is why it admits lowercase path segments only. +check_fixture no no "projected token file path" '/var/run/secrets/kubernetes.io/serviceaccount/token' +check_fixture yes no "standard base64 starting with a slash" '/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgK' + # MUST NOT be detected by either — the real reviewed values these exceptions exist for. check_fixture no no "acme contact address" 'ned@devantler.tech' check_fixture no no "public domain" 'platform.devantler.tech' @@ -279,7 +296,7 @@ rm -f "$selftest_probe" [ "$selftest_failures" -eq 0 ] || fail \ "$selftest_failures detector self-test failure(s). The content-premise checks above cannot be \ trusted until these pass — a detector that no longer matches reports success identically." -echo "PASS(selftest): both content detectors pin their own coverage (9 credential fixtures, 5 cleared, 2 unreadable-input rejections)." +echo "PASS(selftest): both content detectors pin their own coverage (10 credential fixtures, 6 cleared, 2 unreadable-input rejections)." # --- Layer 2: behavioural paired control. Needs trivy; skips (loudly) where it is unavailable. command -v trivy >/dev/null 2>&1 || { @@ -370,6 +387,10 @@ echo " with ignorefile : excepted=$SCOPED_EXCEPTED probe=$SCOPED_PROBE (s readonly REVIEWED_KEYS_prod="admin_email" readonly REVIEWED_KEYS_local="admin_email" readonly REVIEWED_KEYS_actual="const secretKey,enablebanking_secretKey" +# Reviewed with trivy 0.76.1. Single-quoted: the jq source contains `$input` and `$list`. +readonly REVIEWED_KEYS_homepage='Email' +# shellcheck disable=SC2016 +readonly REVIEWED_KEYS_progress='def scope_key,| key,| ( def index_of($list),| ($input.vulnerability | list | map(key,| group_by(.key) | map( key' # Extract the brace-delimited key list trivy reports for one target, normalise it, and join. matched_keys_at() { @@ -429,11 +450,14 @@ to a Secret, or re-review the exception." assert_reviewed_key_sets "$CHECK_ID" \ "k8s/clusters/prod/bootstrap/config-map.yaml:$REVIEWED_KEYS_prod" \ "k8s/clusters/local/bootstrap/config-map.yaml:$REVIEWED_KEYS_local" \ - "k8s/bases/apps/actual-budget/config-map.yaml:$REVIEWED_KEYS_actual" + "k8s/bases/apps/actual-budget/config-map.yaml:$REVIEWED_KEYS_actual" \ + "k8s/bases/apps/homepage/config-map.yaml:$REVIEWED_KEYS_homepage" \ + "k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml:$REVIEWED_KEYS_progress" # --- The KSV-0109 dispositions' content premises. --- # KSV-0109 "ConfigMap stores secrets" is the other ConfigMap check, and it is the one that matches -# credential-shaped KEY NAMES rather than value text. Two files are excepted from it: +# credential-shaped KEY NAMES rather than value text. Three files are excepted from it; the third, +# the generated security-progress ConfigMap, matches only the word "pass" in a script comment. # # k8s/clusters/prod/bootstrap/config-map.yaml — matched on external_secrets_replicas, a public # controller replica count. The word "secrets" @@ -443,14 +467,17 @@ assert_reviewed_key_sets "$CHECK_ID" \ # JavaScript that READS credentials from an # ESO-mounted Secret. # -# Both dispositions are scoped per FILE, so they cover whatever those files hold — including a +# All three dispositions are scoped per FILE, so they cover whatever those files hold — including a # credential added later under a key nobody reviewed. Such an addition moves no count, because the # finding at that path is already suppressed, so neither the scan total nor the path-scoping checks # above can see it. Asserting the matched-key SET is what makes it fail. readonly CHECK_ID_0109="KSV-0109" readonly REVIEWED_0109_prod="external_secrets_replicas" readonly REVIEWED_0109_actual="const secretKey,enablebanking_secretKey" +# The generated security-progress ConfigMap matches only prose in an embedded script comment. +readonly REVIEWED_0109_progress='# FAIL CLOSED. An empty or failed read of any input is UNKNOWN (exit 2),never a clean pass' assert_reviewed_key_sets "$CHECK_ID_0109" \ "k8s/clusters/prod/bootstrap/config-map.yaml:$REVIEWED_0109_prod" \ - "k8s/bases/apps/actual-budget/config-map.yaml:$REVIEWED_0109_actual" + "k8s/bases/apps/actual-budget/config-map.yaml:$REVIEWED_0109_actual" \ + "k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml:$REVIEWED_0109_progress" diff --git a/scripts/tests/test-trivyignore-first-party-rbac-boundary.sh b/scripts/tests/test-trivyignore-first-party-rbac-boundary.sh index 9f321b8fc0..13c2cd0365 100755 --- a/scripts/tests/test-trivyignore-first-party-rbac-boundary.sh +++ b/scripts/tests/test-trivyignore-first-party-rbac-boundary.sh @@ -100,6 +100,12 @@ readonly reviewed_pairs=( "KSV-0048:$kyverno_role" "KSV-0048:$kro_webapp" "KSV-0113:$vault_config_role" + # Premises asserted in section 6 below. + "KSV-0048:k8s/providers/hetzner/infrastructure/controllers/longhorn/role-kyverno-mutate-csi-sidecars.yaml" + "KSV-0048:k8s/providers/hetzner/infrastructure/controllers/longhorn/role-kyverno-mutate-longhorn-csi-plugin.yaml" + "KSV-0048:k8s/providers/hetzner/infrastructure/controllers/longhorn/role-kyverno-mutate-longhorn-engine-image.yaml" + "KSV-0049:k8s/bases/infrastructure/controllers/prune-protected-orphan-alert/role.yaml" + "KSV-0053:k8s/providers/hetzner/apps/world-at-ruin/role-operator.yaml" ) readonly vendored_cdi='k8s/bases/infrastructure/controllers/cdi/cdi-operator.yaml' readonly vendored_kubevirt='k8s/bases/infrastructure/controllers/kubevirt/kubevirt-operator.yaml' @@ -327,5 +333,66 @@ while IFS= read -r rule; do set +f done < <(yq -N '.rules[] | ((.resourceNames // []) | join(",")) + "|" + ((.verbs // []) | join(","))' "$repo_root/$vault_config_role") +# 6. The single-rule namespaced Roles dispositioned after #2990 (Longhorn retrofit, orphan alert, +# World at Ruin trial). Each disposition rests on the same three facts as the umami Kyverno role +# above: a namespaced Role, exactly one rule over the reviewed group/resources, pinned to the +# reviewed resourceNames, with verbs drawn only from the reviewed set. A whitelist under +# `set -f`, for the reason the vault-config check gives: a literal `*` must not glob away. +# args: +assert_single_rule_role() { + local check="$1" role="$2" want_ns="$3" want_shape="$4" want_names="$5" allowed="$6" + local kind ns rules shape names verb + kind="$(yq -N '.kind // ""' "$repo_root/$role" 2>/dev/null || printf '')" + ns="$(yq -N '.metadata.namespace // ""' "$repo_root/$role" 2>/dev/null || printf '')" + rules="$(yq -N '.rules | length' "$repo_root/$role" 2>/dev/null || printf '0')" + if [ "$kind" != "Role" ] || [ "$ns" != "$want_ns" ] || [ "${rules:-0}" -ne 1 ]; then + printf 'PREMISE BROKEN: %s is a %s in %s with %s rule(s); the %s disposition assumes one rule in a Role in %s\n' \ + "$role" "${kind:-unset}" "${ns:-unset}" "${rules:-unset}" "$check" "$want_ns" >&2 + status=1 + return + fi + shape="$(yq -N '.rules[0] | ((.apiGroups // []) | join(",")) + "/" + ((.resources // []) | join(","))' "$repo_root/$role")" + names="$(yq -N '.rules[0] | ((.resourceNames // []) | join(","))' "$repo_root/$role")" + if [ "$shape" != "$want_shape" ] || [ "$names" != "$want_names" ]; then + printf 'PREMISE BROKEN: %s targets [%s] pinned to [%s]; the %s disposition assumes [%s] pinned to [%s]\n' \ + "$role" "$shape" "${names:-}" "$check" "$want_shape" "${want_names:-}" >&2 + status=1 + fi + set -f + while IFS= read -r verb; do + [ -n "$verb" ] || continue + case " $allowed " in + *" $verb "*) ;; + *) + printf 'PREMISE BROKEN: %s grants verb %s; the %s disposition assumes only: %s\n' "$role" "$verb" "$check" "$allowed" >&2 + status=1 + ;; + esac + done < <(yq -N '.rules[0].verbs[]' "$repo_root/$role") + set +f +} + +readonly longhorn_dir='k8s/providers/hetzner/infrastructure/controllers/longhorn' +assert_single_rule_role KSV-0048 "$longhorn_dir/role-kyverno-mutate-csi-sidecars.yaml" longhorn-system \ + 'apps/deployments' 'csi-attacher,csi-provisioner,csi-resizer,csi-snapshotter' 'get patch update' +assert_single_rule_role KSV-0048 "$longhorn_dir/role-kyverno-mutate-longhorn-csi-plugin.yaml" longhorn-system \ + 'apps/daemonsets' 'longhorn-csi-plugin' 'get patch update' +assert_single_rule_role KSV-0048 "$longhorn_dir/role-kyverno-mutate-longhorn-engine-image.yaml" longhorn-system \ + 'apps/daemonsets' 'engine-image-ei-493e04e7' 'get patch update' +assert_single_rule_role KSV-0049 'k8s/bases/infrastructure/controllers/prune-protected-orphan-alert/role.yaml' observability \ + '/configmaps' 'prune-protected-orphan-alert-state' 'get patch' +readonly war_role='k8s/providers/hetzner/apps/world-at-ruin/role-operator.yaml' +assert_single_rule_role KSV-0053 "$war_role" world-at-ruin '/pods/portforward,pods/exec' '' 'create' + +# The World at Ruin exception is owner-only: every binding of the role names exactly one subject, +# the owner's OIDC user. A ServiceAccount or group subject would extend exec beyond the owner. +war_subjects="$(yq -N 'select(.kind == "RoleBinding" and .roleRef.kind == "Role" and .roleRef.name == "world-at-ruin-zone-trial-operator") | .subjects[] | .kind + ":" + .name' "$repo_root"/k8s/providers/hetzner/apps/world-at-ruin/*.yaml 2>/dev/null || printf '')" +# The literal Flux substitution placeholder is the reviewed subject, so it must not expand here. +# shellcheck disable=SC2016 +if [ "$war_subjects" != 'User:oidc:${admin_email}' ]; then + printf 'PREMISE BROKEN: %s is bound to [%s]; the KSV-0053 disposition assumes the owner OIDC user alone\n' "$war_role" "${war_subjects:-}" >&2 + status=1 +fi + [ "$status" -eq 0 ] || exit 1 printf 'first-party RBAC dispositions hold: tenant role never cluster-bound, cluster-reader read-only, KRO aggregation-scoped, kyverno grant pinned to one Deployment, vault-config unscoped only for create\n' diff --git a/scripts/tests/test-trivyignore-private-mail-identity-boundary.sh b/scripts/tests/test-trivyignore-private-mail-identity-boundary.sh new file mode 100755 index 0000000000..669c8fa07f --- /dev/null +++ b/scripts/tests/test-trivyignore-private-mail-identity-boundary.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# The KSV-0020/KSV-0021 dispositions on the private-mail StatefulSet are PATH-scoped, and this test +# is what keeps them honest. +# +# WHY THIS EXISTS +# Stalwart runs at the identity its own image defines: the pinned digest's config User is +# `stalwart`, and its layers carry stalwart:x:2000:2000 in /etc/passwd and stalwart:x:2000 in +# /etc/group. The platform bootstrap image is built FROM that digest and ends with USER 2000:2000, +# and the mail store on the data volume is owned by that identity. trivy reads each file alone and +# scopes an ignore by path, so its entry silences the checks for every container in these files — +# including one added later at some other low identity with no such reason. +# +# Three layers, which fail for different reasons: +# +# STRUCTURE (always) — each check has exactly one entry, scoped to exactly these three files. +# PREMISE (always) — every container's effective UID and GID is 2000, the pod default is that +# identity, and the images are still the measured Stalwart digest and the bootstrap image built +# from it with USER 2000:2000. +# BEHAVIOUR (when trivy is installed) — the StatefulSet's bytes at an unlisted probe path must +# still report both checks with the ignorefile, while the dispositioned path reports neither. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +readonly REPO_ROOT +readonly IGNOREFILE="$REPO_ROOT/.trivyignore.yaml" +readonly SET_PATH='k8s/bases/apps/private-mail/stateful-set.yaml' +readonly SERVE_PATH='k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml' +readonly PROXY_PATH='k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml' +readonly DOCKERFILE='scripts/private-mail-config/Dockerfile' +readonly PROBE_PATH='k8s/bases/apps/trivyignore-private-mail-probe/stateful-set.yaml' +EXPECTED_PATHS="$(printf '%s\n' "$SET_PATH" "$SERVE_PATH" "$PROXY_PATH" | LC_ALL=C sort)" +readonly EXPECTED_PATHS +# The identity was measured from this digest, not from the repository name or tag. +readonly STALWART_IMAGE='ghcr.io/stalwartlabs/stalwart:v0.16.27@sha256:c429b311c93db5606598abc1c68225a85a6953183848ae01bee9c6a98885836c' +readonly BOOTSTRAP_REPOSITORY='ghcr.io/devantler-tech/platform-private-mail-bootstrap' +readonly IMAGE_ID=2000 + +status=0 +fail() { + printf '%s\n' "$*" >&2 + status=1 +} + +command -v yq >/dev/null 2>&1 || { + printf 'yq is required to check the private-mail identity disposition boundary\n' >&2 + exit 1 +} +for path in "$SET_PATH" "$SERVE_PATH" "$PROXY_PATH" "$DOCKERFILE" .trivyignore.yaml; do + [ -r "$REPO_ROOT/$path" ] || { + printf 'not readable: %s\n' "$path" >&2 + exit 1 + } +done + +# ---------------------------------------------------------------- structure -- +for check in KSV-0020 KSV-0021; do + entries="$(SET="$SET_PATH" CHECK="$check" yq '[.misconfigurations[] | select(.paths // [] | contains([strenv(SET)])) + | select(.id == strenv(CHECK))] | length' "$IGNOREFILE" 2>/dev/null)" || entries='' + if [ "$entries" != 1 ]; then + fail "STRUCTURE: $check must have exactly one entry naming $SET_PATH, found ${entries:-unreadable}" + continue + fi + paths="$(SET="$SET_PATH" CHECK="$check" yq -r '.misconfigurations[] | select(.paths // [] | contains([strenv(SET)])) + | select(.id == strenv(CHECK)) | .paths[]' "$IGNOREFILE" | LC_ALL=C sort)" + [ "$paths" = "$EXPECTED_PATHS" ] || fail "STRUCTURE: the $check entry is scoped to [$paths], not the three reviewed files" +done + +# ------------------------------------------------------------------ premise -- +# Effective identity per container: the container's own value, else the pod's. The add-client-proxy +# fragment has no pod securityContext; its pod is serve-after-initialization's, so that is its +# default. +pod_value() { yq -r ".spec.template.spec.securityContext.$2 // \"\"" "$REPO_ROOT/$1"; } +for field in runAsUser runAsGroup; do + for file in "$SET_PATH" "$SERVE_PATH"; do + [ "$(pod_value "$file" "$field")" = "$IMAGE_ID" ] || fail "PREMISE: $file pod $field is not $IMAGE_ID" + done + for file in "$SET_PATH" "$SERVE_PATH" "$PROXY_PATH"; do + default="$(pod_value "$file" "$field")" + [ -n "$default" ] || default="$(pod_value "$SERVE_PATH" "$field")" + while IFS='|' read -r name value; do + [ -n "$name" ] || continue + [ "${value:-$default}" = "$IMAGE_ID" ] || + fail "PREMISE: container $name in $file runs with $field ${value:-$default}, not the image-defined $IMAGE_ID" + done < <(yq -r "[(.spec.template.spec.initContainers // [])[], (.spec.template.spec.containers // [])[]] + | .[] | .name + \"|\" + ((.securityContext.$field // \"\") | tostring)" "$REPO_ROOT/$file") + done +done + +[ "$(yq -r '.spec.template.spec.containers[] | select(.name == "private-mail") | .image' "$REPO_ROOT/$SET_PATH")" = "$STALWART_IMAGE" ] || + fail "PREMISE: $SET_PATH no longer runs the measured Stalwart digest; re-measure its /etc/passwd identity" +for file in "$SERVE_PATH" "$PROXY_PATH"; do + while IFS= read -r image; do + [ "$image" = "$BOOTSTRAP_REPOSITORY" ] || fail "PREMISE: $file runs $image, not the bootstrap image built from the measured digest" + done < <(yq -r '[(.spec.template.spec.initContainers // [])[], (.spec.template.spec.containers // [])[]] | .[].image' "$REPO_ROOT/$file") +done +[ "$(awk 'toupper($1) == "FROM" {base = $2} END {print base}' "$REPO_ROOT/$DOCKERFILE")" = "$STALWART_IMAGE" ] || + fail "PREMISE: the bootstrap image's final stage is no longer built from the measured Stalwart digest" +[ "$(awk 'toupper($1) == "USER" {user = $2} END {print user}' "$REPO_ROOT/$DOCKERFILE")" = "$IMAGE_ID:$IMAGE_ID" ] || + fail "PREMISE: the bootstrap image no longer ends with USER $IMAGE_ID:$IMAGE_ID" + +[ "$status" -eq 0 ] || exit 1 +echo "PASS(structural): KSV-0020/KSV-0021 are scoped to the three private-mail files, and every container there runs as $IMAGE_ID:$IMAGE_ID." + +# ---------------------------------------------------------------- behaviour -- +command -v trivy >/dev/null 2>&1 || { + echo "SKIP(behavioural): trivy not installed; structural and premise checks above still passed" + exit 0 +} +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +mkdir -p "$work/$(dirname "$SET_PATH")" "$work/$(dirname "$PROBE_PATH")" "$work/.trivy" +cp "$REPO_ROOT/$SET_PATH" "$work/$SET_PATH" +cp "$REPO_ROOT/$SET_PATH" "$work/$PROBE_PATH" +cp "$IGNOREFILE" "$work/.trivyignore.yaml" +cp -R "$REPO_ROOT/.trivy/data" "$work/.trivy/data" +(cd "$work" && trivy fs --scanners misconfig --config-data .trivy/data --ignorefile .trivyignore.yaml \ + --format json .) >"$work/out.json" 2>/dev/null +count_at() { + jq -r --arg t "$1" --arg id "$2" \ + '[ .Results[]? | select(.Target == $t) | .Misconfigurations[]? | select(.ID == $id and .Status == "FAIL") ] | length' \ + "$work/out.json" +} +for check in KSV-0020 KSV-0021; do + [ "$(count_at "$PROBE_PATH" "$check")" -gt 0 ] || + fail "VACUOUS OR BREACHED: $check does not report the same bytes at the unlisted $PROBE_PATH" + [ "$(count_at "$SET_PATH" "$check")" -eq 0 ] || fail "UNCOVERED: $check still reports $SET_PATH" +done +[ "$status" -eq 0 ] || exit 1 +echo "PASS(behavioural): identical bytes report at the probe path and are suppressed only at the reviewed path." diff --git a/scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh b/scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh index 5480f7eee8..cbc78055a3 100755 --- a/scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh +++ b/scripts/tests/test-trivyignore-vault-backup-identity-boundary.sh @@ -76,6 +76,10 @@ readonly REVIEWED_ELSEWHERE=( 'k8s/bases/infrastructure/controllers/cdi/cdi-operator.yaml' 'k8s/bases/infrastructure/controllers/kubevirt/kubevirt-operator.yaml' 'k8s/bases/infrastructure/vault-config/job.yaml' + # Stalwart's image-baked identity, guarded by test-trivyignore-private-mail-identity-boundary.sh. + 'k8s/bases/apps/private-mail/stateful-set.yaml' + 'k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml' + 'k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml' ) # A Job keeps its pod spec one level down; a CronJob keeps it under jobTemplate. Selecting whichever diff --git a/scripts/tests/test-trivyignore-vault-config-identity-boundary.sh b/scripts/tests/test-trivyignore-vault-config-identity-boundary.sh index 078e4cf2df..272db8fcbb 100755 --- a/scripts/tests/test-trivyignore-vault-config-identity-boundary.sh +++ b/scripts/tests/test-trivyignore-vault-config-identity-boundary.sh @@ -68,6 +68,12 @@ readonly VENDORED_KUBEVIRT='k8s/bases/infrastructure/controllers/kubevirt/kubevi readonly VAULT_BACKUP_JOB='k8s/bases/infrastructure/vault-backup/job.yaml' readonly VAULT_BACKUP_CRON='k8s/bases/infrastructure/vault-backup/cron-job.yaml' +# The private-mail StatefulSet and its two overlay fragments carry their own reviewed disposition +# on Stalwart's image-baked identity, guarded by test-trivyignore-private-mail-identity-boundary.sh. +readonly PRIVATE_MAIL_SET='k8s/bases/apps/private-mail/stateful-set.yaml' +readonly PRIVATE_MAIL_SERVE='k8s/bases/apps/private-mail-administration/patches/serve-after-initialization.yaml' +readonly PRIVATE_MAIL_PROXY='k8s/bases/apps/private-mail-proxy-identity/patches/add-client-proxy.yaml' + status=0 fail() { printf '%s\n' "$*" >&2 @@ -127,6 +133,7 @@ for CHECK_ID in "${CHECK_IDS[@]}"; do [ -n "$path" ] || continue case "$path" in "$VENDORED_CDI" | "$VENDORED_KUBEVIRT" | "$JOB_PATH" | "$VAULT_BACKUP_JOB" | "$VAULT_BACKUP_CRON") ;; + "$PRIVATE_MAIL_SET" | "$PRIVATE_MAIL_SERVE" | "$PRIVATE_MAIL_PROXY") ;; *) fail "UNREVIEWED DISPOSITION: $CHECK_ID is scoped to $path, which no premises test guards" ;; esac done < Date: Tue, 3 Nov 2026 23:16:26 +0100 Subject: [PATCH 11/19] fix(lint): clear MegaLinter's YAML and workflow findings for the org Go workflow The org validate-go-project workflow runs MegaLinter v10.1.0 over the whole repository. This clears its yamllint, v8r, zizmor, checkov (CKV_GHA_*) and shellcheck findings without changing what any workflow, patch or fixture does. - yamllint (293 in CI, 304 locally before prettier's in-place fixes): add the `---` document marker to 223 files and quote the workflow trigger key as `"on":` in 63 workflows. Every changed YAML file except two parses to identical data. Re-indent three comments that sat outside their content, space the flow sequences in one fixture, drop a trailing blank line, and wrap four lines over 500 characters: the deploy-prod readiness error is now one echo with several arguments (same single-line output), and the CEL in scripts/tests/crossplane-egress-policy-rules.yaml is re-wrapped between tokens (identical parsed AST). The one test that read the trigger block as text (`/^on:/` in test-probe-cilium-externalauth-crossnode.sh) now accepts the quoted key and fails on an empty block, where its no-other-trigger check would otherwise have passed on nothing. - talos/factory-schematic.yaml keeps no marker: its sha256 is the Image Factory schematic ID registered before deploy and read back from nodes. A repository .yamllint.yml (MegaLinter's template plus one path-scoped document-start ignore) records that, and .prettierignore stops MegaLinter's APPLY_FIXES from re-indenting it into a different ID. - v8r: rename the CNPG fixture off `cluster.yaml`, which SchemaStore maps to the RKE cluster schema; the test still copies it in as cluster.yaml. - zizmor: MegaLinter's zizmor 1.25.0 flags `owner` on five app-token steps that already set `repositories`, because it looks for a singular `repository` input (zizmorcore/zizmor#2011, fixed in 1.25.1). Each `owner` line carries an inline github-app ignore with that reason; no token scope, permission or input changes. - checkov CKV_GHA_7: three dispatch-only workflows build nothing; their inputs are a typed interlock, an approval binding and a dry-run default. Each gets a file-scoped checkov skip with that reason, as dr-rebuild.yaml already has. - shellcheck SC2153 in bootstrap-tenant-source.sh was a symptom: shfmt 3.13.1 treats a U+0080 character as end of input and truncated generate-publish-workflow-approved-revisions.sh (630 -> 2 lines) before shellcheck read its sourcer. Its comments held double-encoded mojibake; they are restored to the intended em dashes, arrows and rocket emoji. Part of devantler-tech/platform#5852 Co-Authored-By: Claude Opus 5.5 --- .coderabbit.yaml | 1 + .github/actionlint.yaml | 1 + .github/actions/deploy-prod/action.yml | 7 +++- .../publish-platform-manifests/action.yml | 1 + .github/dependabot.yaml | 1 + .github/manifest-naming.yaml | 1 + .github/workflows/cd.yaml | 3 +- .github/workflows/ci.yaml | 3 +- .../cleanup-orphan-generated-vpas.yaml | 3 +- .github/workflows/copilot-setup-steps.yml | 3 +- .../copy-email-credential-custody.yaml | 1 + .../diagnose-cilium-ext-authz-ipcache.yaml | 3 +- .github/workflows/diagnose-wedding-pause.yaml | 3 +- .github/workflows/dr-rebuild.yaml | 3 +- .github/workflows/expand-openbao-audit.yaml | 3 +- .github/workflows/measure-ci-runtime.yaml | 3 +- .../measure-datapath-cpu-throttling.yaml | 3 +- .../observe-email-data-recovery.yaml | 1 + .../observe-historical-longhorn-backups.yaml | 3 +- .../observe-longhorn-migration-headroom.yaml | 3 +- .../observe-longhorn-namespace-inventory.yaml | 3 +- .github/workflows/observe-runtime-health.yaml | 3 +- .../workflows/prepare-email-storage-key.yaml | 1 + .../workflows/prepare-longhorn-v2-node.yaml | 3 +- .../probe-cilium-externalauth-crossnode.yaml | 3 +- .../workflows/probe-gateway-denied-peer.yaml | 3 +- .../probe-github-team-external-identity.yaml | 3 +- .../probe-image-signature-enforcement.yaml | 3 +- .../workflows/prove-cilium-default-deny.yaml | 6 +++- .../prove-kyverno-cache-recovery.yaml | 3 +- .../prove-kyverno-stale-report-prune.yaml | 3 +- .../workflows/prove-longhorn-ui-readonly.yaml | 3 +- .../prove-scoped-package-access.yaml | 8 +++-- .../publish-coroot-checkpoint-hotfix.yaml | 3 +- .../publish-coroot-node-agent-hotfix.yaml | 3 +- .../publish-kubescape-node-agent-hotfix.yaml | 3 +- .../publish-kubescape-storage-hotfix.yaml | 3 +- .../publish-kyverno-background-hotfix.yaml | 3 +- .../publish-kyverno-migration-runtime.yaml | 3 +- .../publish-kyverno-reports-hotfix.yaml | 3 +- .../publish-private-mail-bootstrap.yaml | 3 +- .../workflows/publish-release-recovery.yaml | 11 +++++-- .../publish-unsigned-probe-image.yaml | 3 +- .../read-actual-budget-userns-maps.yaml | 3 +- .../workflows/read-cilium-api-limiter.yaml | 3 +- .../read-crossplane-alert-recovery.yaml | 3 +- .../workflows/read-email-ingress-source.yaml | 3 +- .github/workflows/read-email-probe-state.yaml | 3 +- .../workflows/read-email-startup-state.yaml | 3 +- .../recover-retained-wedding-standby.yaml | 3 +- .../refresh-email-recovery-backup.yaml | 1 + ...e-publish-workflow-approved-revisions.yaml | 33 +++++++++++-------- .github/workflows/release.yaml | 3 +- .../remove-wedding-shared-backups.yaml | 3 +- .../workflows/replace-wedding-standby.yaml | 3 +- .github/workflows/restore-stranded-prod.yaml | 3 +- .../workflows/scan-go-vulnerabilities.yaml | 3 +- .github/workflows/stale-issues.yaml | 4 ++- .github/workflows/todos.yaml | 3 +- .github/workflows/update-skills.yaml | 3 +- .../validate-coroot-security-progress.yaml | 3 +- .../validate-image-verifier-liveness.yaml | 3 +- .github/workflows/validate-main.yaml | 3 +- .../workflows/verify-alert-publisher-app.yaml | 5 ++- .../workflows/verify-arc-app-identity.yaml | 3 +- .../verify-arc-runner-group-capability.yaml | 3 +- .github/workflows/verify-arc-transport.yaml | 3 +- .../verify-email-credential-custody.yaml | 1 + .../workflows/verify-email-data-recovery.yaml | 1 + .../workflows/verify-email-provider-smtp.yaml | 1 + .../verify-email-storage-canary.yaml | 1 + .../verify-email-storage-key-recovery.yaml | 1 + .../workflows/verify-heartbeat-cleanup.yaml | 1 + .../verify-openbao-backup-recovery.yaml | 1 + .../verify-private-mail-storage-recovery.yaml | 1 + .../verify-wedding-backup-bootstrap.yaml | 3 +- .../verify-wedding-backup-cutover.yaml | 3 +- .../verify-wedding-backup-denial.yaml | 3 +- .../verify-wedding-dedicated-restore.yaml | 3 +- .github/workflows/verify-wedding-fence.yaml | 3 +- ...verify-wedding-shared-backup-coverage.yaml | 3 +- .mega-linter.yml | 1 + .prettierignore | 5 +++ .sops.yaml | 1 + .trivy/data/trusted-registries.yaml | 1 + .trivyignore.yaml | 1 + .yamllint.yml | 23 +++++++++++++ ...ate-publish-workflow-approved-revisions.sh | 14 ++++---- .../tests/crossplane-egress-policy-rules.yaml | 18 +++++++--- .../tests/fixtures/coroot-dns-probe/pod.yaml | 1 + .../{cluster.yaml => cnpg-cluster.yaml} | 1 + .../tests/isolated-chart-namespace-rules.yaml | 1 + .../tests/kyverno-admission-vpa-rules.yaml | 1 + .../tests/production-authorization-rules.yaml | 1 + scripts/tests/test-hcloud-volume-priority.sh | 4 ++- ...est-probe-cilium-externalauth-crossnode.sh | 4 ++- .../allow-scheduling-on-control-planes.yaml | 1 + .../disable-default-cni-and-kube-proxy.yaml | 1 + talos-local/cluster/enable-dex-oidc.yaml | 1 + .../cluster/rotate-server-certificates.yaml | 1 + talos/cluster/authenticate-ghcr-pulls.yaml | 1 + talos/cluster/block-ingress-by-default.yaml | 1 + .../disable-default-cni-and-kube-proxy.yaml | 1 + talos/cluster/enable-apparmor.yaml | 11 ++++--- talos/cluster/enable-audit-logging.yaml | 1 + talos/cluster/enable-dex-oidc.yaml | 1 + talos/cluster/enable-user-namespaces.yaml | 1 + talos/cluster/encrypt-ephemeral-volume.yaml | 1 + talos/cluster/encrypt-state-volume.yaml | 1 + talos/cluster/evict-pods-before-oom.yaml | 1 + talos/cluster/gc-terminated-pods-sooner.yaml | 1 + .../gc-unused-images-before-pressure.yaml | 1 + talos/cluster/harden-kernel-sysctls.yaml | 1 + talos/cluster/mark-ghcr-pull-revision.yaml | 1 + talos/cluster/pack-pods-onto-busy-nodes.yaml | 1 + talos/cluster/rotate-server-certificates.yaml | 1 + talos/cluster/use-platform-hostname.yaml | 1 + talos/cluster/verify-first-party-images.yaml | 1 + .../allow-internal-node-ingress.yaml | 1 + .../allow-internal-nodepod-ingress.yaml | 1 + .../allow-internal-udp-ingress.yaml | 1 + .../control-planes/allow-public-ingress.yaml | 1 + .../allow-wireguard-server-ingress.yaml | 1 + talos/control-planes/wireguard.yaml | 1 + talos/workers/allow-apid-ingress.yaml | 1 + .../workers/allow-internal-node-ingress.yaml | 1 + .../allow-internal-nodepod-ingress.yaml | 1 + talos/workers/allow-internal-udp-ingress.yaml | 1 + talos/workers/enable-longhorn-v2.yaml | 1 + talos/workers/label-nodes.yaml | 1 + talos/workers/load-kvm-modules.yaml | 1 + talos/workers/mount-longhorn-data.yaml | 1 + talos/workers/reserve-ephemeral-space.yaml | 1 + talos/workers/reserve-longhorn-data.yaml | 1 + tests/audit-privileged-rbac/kyverno-test.yaml | 10 +++--- tests/audit-privileged-rbac/resources.yaml | 14 ++++---- .../csi-sidecar-baseline-context/targets.yaml | 1 + .../csi-sidecar-baseline-context/trigger.yaml | 1 + tests/disallow-latest-tag/kyverno-test.yaml | 1 + tests/disallow-latest-tag/resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../missing-reliability/kyverno-test.yaml | 1 + .../missing-reliability/resources.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../label-removed/kyverno-test.yaml | 1 + .../label-removed/values.yaml | 1 + .../patched-resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../patched-resources.yaml | 1 - .../kyverno-test.yaml | 1 + .../label-removed/kyverno-test.yaml | 1 + .../label-removed/values.yaml | 1 + .../patched-resources.yaml | 1 + .../kyverno-stale-report-prune/fixtures.yaml | 1 + .../unevaluated-namespace-fixtures.yaml | 1 + tests/kyverno-stale-report-prune/values.yaml | 1 + .../patched-resources.yaml | 1 + tests/label-baseline-workers/resources.yaml | 1 + .../already-clean.yaml | 1 + .../near-misses.yaml | 1 + .../resources.yaml | 1 + .../trigger.yaml | 1 + .../already-clean.yaml | 1 + .../near-misses.yaml | 1 + .../resources.yaml | 1 + .../trigger.yaml | 1 + .../kyverno-test.yaml | 1 + .../longhorn-registrar-prestop/resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../patched-resources.yaml | 1 + tests/prioritise-database-pods/resources.yaml | 1 + .../update/kyverno-test.yaml | 1 + .../update/values.yaml | 1 + tests/prioritise-database-pods/values.yaml | 1 + .../context.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../node-group-resources.yaml | 1 + .../restrict-tenant-webapps/kyverno-test.yaml | 1 + tests/restrict-tenant-webapps/resources.yaml | 1 + tests/restrict-tenant-webapps/user-info.yaml | 1 + .../kyverno-test.yaml | 1 + .../ordinary-writer/kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../values.yaml | 1 + .../kyverno-test.yaml | 1 + .../resources.yaml | 1 + .../user-info.yaml | 1 + .../validate-pdb-drain-safe/kyverno-test.yaml | 1 + tests/validate-pdb-drain-safe/resources.yaml | 1 + .../kyverno-test.yaml | 1 + tests/validate-replica-ceiling/resources.yaml | 1 + 232 files changed, 395 insertions(+), 112 deletions(-) create mode 100644 .prettierignore create mode 100644 .yamllint.yml rename scripts/tests/fixtures/wedding-database-priority/{cluster.yaml => cnpg-cluster.yaml} (98%) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 6fc0a61212..9197f42a8a 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,3 +1,4 @@ +--- # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json # # CodeRabbit configuration. Only the settings that differ from CodeRabbit's defaults are diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 154c39d477..15bc4ac36a 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -1,3 +1,4 @@ +--- # actionlint configuration. # # `concurrency.queue` is a real, documented GitHub Actions key — SchemaStore's diff --git a/.github/actions/deploy-prod/action.yml b/.github/actions/deploy-prod/action.yml index 2ca1e8313e..3a0f8ef45e 100644 --- a/.github/actions/deploy-prod/action.yml +++ b/.github/actions/deploy-prod/action.yml @@ -1,3 +1,4 @@ +--- name: Deploy to Production description: >- Deploy the platform's GitOps manifests to the production Talos + Hetzner @@ -176,7 +177,11 @@ runs: sleep 10 done last_err=$(printf '%s' "$last_err" | tr '\n' ' ') - echo "::error::Prod Kubernetes API (context admin@prod) failed its readiness probe after stable endpoint selection (last kubectl error: ${last_err}). Inspect the selected endpoint, network reachability, and API readiness, then diagnose any reported TLS, authentication, or authorization failure. Recover kubeconfig credentials only if that diagnosis confirms a credential failure; this Kubernetes probe does not test Talos credentials. See docs/dr/runbook.md → 'Endpoint selection during ordinary deployments' and, for confirmed credential failures, 'Credential refresh after a full rebuild'." + # echo joins its arguments with single spaces: the annotation is still one line. + echo "::error::Prod Kubernetes API (context admin@prod) failed its readiness probe after stable endpoint selection (last kubectl error: ${last_err})." \ + "Inspect the selected endpoint, network reachability, and API readiness, then diagnose any reported TLS, authentication, or authorization failure." \ + "Recover kubeconfig credentials only if that diagnosis confirms a credential failure; this Kubernetes probe does not test Talos credentials." \ + "See docs/dr/runbook.md → 'Endpoint selection during ordinary deployments' and, for confirmed credential failures, 'Credential refresh after a full rebuild'." exit 1 - name: 🧱 Register and verify the Talos Image Factory schematic diff --git a/.github/actions/deploy-prod/publish-platform-manifests/action.yml b/.github/actions/deploy-prod/publish-platform-manifests/action.yml index 4a5d917a8d..3d0e556ae4 100644 --- a/.github/actions/deploy-prod/publish-platform-manifests/action.yml +++ b/.github/actions/deploy-prod/publish-platform-manifests/action.yml @@ -1,3 +1,4 @@ +--- name: Publish Platform Manifests description: >- Publish platform manifests through a unique staging reference, attach the diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index b8ceba2971..b04dac2b9f 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,3 +1,4 @@ +--- version: 2 updates: - package-ecosystem: "github-actions" diff --git a/.github/manifest-naming.yaml b/.github/manifest-naming.yaml index c32d7c61eb..5b7413d5ea 100644 --- a/.github/manifest-naming.yaml +++ b/.github/manifest-naming.yaml @@ -1,3 +1,4 @@ +--- version: 1 resource-roots: [k8s] patch-roots: ["talos*"] diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index d0ab7f6da6..2f1020a015 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -1,3 +1,4 @@ +--- name: CD # Manual-only. Production deploys happen automatically in the CI merge queue @@ -5,7 +6,7 @@ name: CD # direct-push-to-main case, which has no merge_group event to deploy. Run it # by hand from the Actions tab. (Dropped the 'v*' release-tag trigger — it # re-deployed what the merge queue had already shipped.) -on: +"on": workflow_dispatch: inputs: verify-wedding-backup-staging: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8a53732fe7..1ced492ef9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,5 +1,6 @@ +--- name: CI -on: +"on": pull_request: branches: [main] merge_group: diff --git a/.github/workflows/cleanup-orphan-generated-vpas.yaml b/.github/workflows/cleanup-orphan-generated-vpas.yaml index 157db96833..280d63cee6 100644 --- a/.github/workflows/cleanup-orphan-generated-vpas.yaml +++ b/.github/workflows/cleanup-orphan-generated-vpas.yaml @@ -1,5 +1,6 @@ +--- name: Clean Up Orphan Generated VPAs -on: +"on": workflow_dispatch: inputs: mode: diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 57bd2a41ec..fd5b74c526 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -1,6 +1,7 @@ +--- name: "Copilot Setup Steps" -on: +"on": workflow_dispatch: push: paths: diff --git a/.github/workflows/copy-email-credential-custody.yaml b/.github/workflows/copy-email-credential-custody.yaml index 859c648c0c..47797ef313 100644 --- a/.github/workflows/copy-email-credential-custody.yaml +++ b/.github/workflows/copy-email-credential-custody.yaml @@ -1,3 +1,4 @@ +--- name: Copy Email Credential Custody "on": diff --git a/.github/workflows/diagnose-cilium-ext-authz-ipcache.yaml b/.github/workflows/diagnose-cilium-ext-authz-ipcache.yaml index 0e57381018..d933b2ce8f 100644 --- a/.github/workflows/diagnose-cilium-ext-authz-ipcache.yaml +++ b/.github/workflows/diagnose-cilium-ext-authz-ipcache.yaml @@ -1,3 +1,4 @@ +--- name: Diagnose Cilium ExternalAuth ipcache # Obtain the ONE read #2284 needs: on a node running no oauth2-proxy replica, is the host-sourced @@ -50,7 +51,7 @@ name: Diagnose Cilium ExternalAuth ipcache # The script's own test needs no cluster and no secrets, so it runs in ci.yaml's required # `validate` job rather than here. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/diagnose-wedding-pause.yaml b/.github/workflows/diagnose-wedding-pause.yaml index 5422dc7834..74f05f2793 100644 --- a/.github/workflows/diagnose-wedding-pause.yaml +++ b/.github/workflows/diagnose-wedding-pause.yaml @@ -1,6 +1,7 @@ +--- name: Diagnose Wedding Pause -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/dr-rebuild.yaml b/.github/workflows/dr-rebuild.yaml index 23b25fb7f7..87e2d83408 100644 --- a/.github/workflows/dr-rebuild.yaml +++ b/.github/workflows/dr-rebuild.yaml @@ -1,3 +1,4 @@ +--- # DR — Rebuild Prod: the executable form of docs/dr/runbook.md Scenario 4 # ("full cluster rebuild from zero"), runnable as one button press when the # production cluster is gone. @@ -37,7 +38,7 @@ name: DR - Rebuild Prod # safe (nothing supersedes) but silently gives up the coalescing. run-name: DR - Rebuild Prod (restore=${{ inputs.restore }}) -on: +"on": # checkov:skip=CKV_GHA_7:The `confirm` input IS the safety interlock — an operator must type # REBUILD-PROD before this workflow will destroy and rebuild the production cluster. Removing # the inputs to satisfy the check would delete that guard and make an accidental dispatch diff --git a/.github/workflows/expand-openbao-audit.yaml b/.github/workflows/expand-openbao-audit.yaml index 42dd19cac9..791362d0bc 100644 --- a/.github/workflows/expand-openbao-audit.yaml +++ b/.github/workflows/expand-openbao-audit.yaml @@ -1,6 +1,7 @@ +--- name: Expand OpenBao Audit Capacity -on: +"on": workflow_dispatch: inputs: mode: diff --git a/.github/workflows/measure-ci-runtime.yaml b/.github/workflows/measure-ci-runtime.yaml index f31b466ec3..5846dc27b5 100644 --- a/.github/workflows/measure-ci-runtime.yaml +++ b/.github/workflows/measure-ci-runtime.yaml @@ -1,5 +1,6 @@ +--- name: Measure CI Runtime -on: +"on": pull_request: branches: [main] paths: [.github/workflows/measure-ci-runtime.yaml] diff --git a/.github/workflows/measure-datapath-cpu-throttling.yaml b/.github/workflows/measure-datapath-cpu-throttling.yaml index c07d4f894f..dcaece8c61 100644 --- a/.github/workflows/measure-datapath-cpu-throttling.yaml +++ b/.github/workflows/measure-datapath-cpu-throttling.yaml @@ -1,3 +1,4 @@ +--- name: Measure Datapath CPU Throttling # Record the throttled share of CFS periods for the datapath DaemonSets (#3790), before and after @@ -31,7 +32,7 @@ name: Measure Datapath CPU Throttling # scripts/tests/test-measure-datapath-cpu-throttling.sh pins the script's behaviour and the shape of # this workflow. It needs no cluster and no secrets, so it runs in ci.yaml's required `validate` job. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/observe-email-data-recovery.yaml b/.github/workflows/observe-email-data-recovery.yaml index f946ce2c46..91f5d989a5 100644 --- a/.github/workflows/observe-email-data-recovery.yaml +++ b/.github/workflows/observe-email-data-recovery.yaml @@ -1,3 +1,4 @@ +--- name: Observe Retained Email Recovery "on": diff --git a/.github/workflows/observe-historical-longhorn-backups.yaml b/.github/workflows/observe-historical-longhorn-backups.yaml index 9f8fad847c..346be12176 100644 --- a/.github/workflows/observe-historical-longhorn-backups.yaml +++ b/.github/workflows/observe-historical-longhorn-backups.yaml @@ -1,6 +1,7 @@ +--- name: Observe Historical Longhorn Backups -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/observe-longhorn-migration-headroom.yaml b/.github/workflows/observe-longhorn-migration-headroom.yaml index ca3b14b5f5..b304f5defd 100644 --- a/.github/workflows/observe-longhorn-migration-headroom.yaml +++ b/.github/workflows/observe-longhorn-migration-headroom.yaml @@ -1,6 +1,7 @@ +--- name: Observe Longhorn Migration Headroom -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/observe-longhorn-namespace-inventory.yaml b/.github/workflows/observe-longhorn-namespace-inventory.yaml index 459d03db24..18407695e9 100644 --- a/.github/workflows/observe-longhorn-namespace-inventory.yaml +++ b/.github/workflows/observe-longhorn-namespace-inventory.yaml @@ -1,6 +1,7 @@ +--- name: Observe Longhorn Namespace Inventory -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/observe-runtime-health.yaml b/.github/workflows/observe-runtime-health.yaml index 84641bf1de..c42198fd63 100644 --- a/.github/workflows/observe-runtime-health.yaml +++ b/.github/workflows/observe-runtime-health.yaml @@ -1,7 +1,8 @@ +--- name: Observe Runtime Health Read Only # Observation only: no object, alert, exception or configuration is changed. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/prepare-email-storage-key.yaml b/.github/workflows/prepare-email-storage-key.yaml index 36205ae3a1..7a48d6f92c 100644 --- a/.github/workflows/prepare-email-storage-key.yaml +++ b/.github/workflows/prepare-email-storage-key.yaml @@ -1,3 +1,4 @@ +--- name: Prepare Email Storage Key "on": diff --git a/.github/workflows/prepare-longhorn-v2-node.yaml b/.github/workflows/prepare-longhorn-v2-node.yaml index 8619ec5862..49a0d1a773 100644 --- a/.github/workflows/prepare-longhorn-v2-node.yaml +++ b/.github/workflows/prepare-longhorn-v2-node.yaml @@ -1,6 +1,7 @@ +--- name: Prepare Longhorn V2 Migration -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/probe-cilium-externalauth-crossnode.yaml b/.github/workflows/probe-cilium-externalauth-crossnode.yaml index 4dd55aec48..a2116a62f8 100644 --- a/.github/workflows/probe-cilium-externalauth-crossnode.yaml +++ b/.github/workflows/probe-cilium-externalauth-crossnode.yaml @@ -1,3 +1,4 @@ +--- name: Probe Cilium ExternalAuth Cross-Node # Answer #2284 with a datapath test (#3784): does the gateway Envoy's Gateway API ExternalAuth @@ -38,7 +39,7 @@ name: Probe Cilium ExternalAuth Cross-Node # shape of this workflow. It needs no cluster and no secrets, so it runs in ci.yaml's required # `validate` job rather than here. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/probe-gateway-denied-peer.yaml b/.github/workflows/probe-gateway-denied-peer.yaml index 31f92ae0e1..cc51598519 100644 --- a/.github/workflows/probe-gateway-denied-peer.yaml +++ b/.github/workflows/probe-gateway-denied-peer.yaml @@ -1,3 +1,4 @@ +--- name: Probe Gateway Denied Peer # Answers the first question of #5448 stage 3: does the Gateway's TLS fence refuse a physical peer @@ -11,7 +12,7 @@ name: Probe Gateway Denied Peer # script, the main-branch guard, the confirmation phrase and `contents: read`. The log carries only # counts, curl exit codes and the verdict. scripts/tests/test-probe-gateway-denied-peer.sh pins # this in CI's required `validate` job without a cluster. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/probe-github-team-external-identity.yaml b/.github/workflows/probe-github-team-external-identity.yaml index 08028aef58..74a3645bf6 100644 --- a/.github/workflows/probe-github-team-external-identity.yaml +++ b/.github/workflows/probe-github-team-external-identity.yaml @@ -1,3 +1,4 @@ +--- name: Probe GitHub Team External Identity # Obtain the ONE observation #3144 is missing: that production admission really refuses a @@ -32,7 +33,7 @@ name: Probe GitHub Team External Identity # ServiceAccount name, and never kubectl's own error text. The endpoint helper prints no address # either; its standard output is discarded so this log carries the probe's lines only. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/probe-image-signature-enforcement.yaml b/.github/workflows/probe-image-signature-enforcement.yaml index 91c714a4a6..27b2c303be 100644 --- a/.github/workflows/probe-image-signature-enforcement.yaml +++ b/.github/workflows/probe-image-signature-enforcement.yaml @@ -1,3 +1,4 @@ +--- name: Probe Image Signature Enforcement # Obtain the ONE observation #3336 is missing: that a matched-but-unsigned image @@ -45,7 +46,7 @@ name: Probe Image Signature Enforcement # so a cached image is never re-pulled and the probe would verify nothing; it # fails closed (INCONCLUSIVE) rather than reporting that as a pass. -on: +"on": workflow_dispatch: inputs: node: diff --git a/.github/workflows/prove-cilium-default-deny.yaml b/.github/workflows/prove-cilium-default-deny.yaml index 30721f31b1..c06c4c519a 100644 --- a/.github/workflows/prove-cilium-default-deny.yaml +++ b/.github/workflows/prove-cilium-default-deny.yaml @@ -1,8 +1,12 @@ +--- name: Prove Cilium Default Deny # Dormant proof plumbing for #3501. Dispatch requires a separately approved # main workflow SHA and candidate PR/head. No production access or provider. -on: +"on": + # checkov:skip=CKV_GHA_7:Not a build. `confirm` is the typed interlock and the other three + # inputs bind the run to the separately approved workflow SHA and candidate PR head; the + # proof publishes no artifact, so removing them would only remove that approval binding. workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/prove-kyverno-cache-recovery.yaml b/.github/workflows/prove-kyverno-cache-recovery.yaml index acee1dde42..a3b16e0801 100644 --- a/.github/workflows/prove-kyverno-cache-recovery.yaml +++ b/.github/workflows/prove-kyverno-cache-recovery.yaml @@ -1,6 +1,7 @@ +--- name: Prove Kyverno Cache Recovery -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/prove-kyverno-stale-report-prune.yaml b/.github/workflows/prove-kyverno-stale-report-prune.yaml index 099b8ecfe8..e39a814a51 100644 --- a/.github/workflows/prove-kyverno-stale-report-prune.yaml +++ b/.github/workflows/prove-kyverno-stale-report-prune.yaml @@ -1,8 +1,9 @@ +--- name: Prove Kyverno Stale Report Prune # #2573: prove the stale-report DeletingPolicy on a throwaway kind cluster # before it reaches production. Runs no production credentials. -on: +"on": pull_request: paths: - .github/workflows/prove-kyverno-stale-report-prune.yaml diff --git a/.github/workflows/prove-longhorn-ui-readonly.yaml b/.github/workflows/prove-longhorn-ui-readonly.yaml index c8b685a6a5..373371463e 100644 --- a/.github/workflows/prove-longhorn-ui-readonly.yaml +++ b/.github/workflows/prove-longhorn-ui-readonly.yaml @@ -1,8 +1,9 @@ +--- name: Prove Longhorn UI Read Only # Fixed, harmless requests from the existing authentication proxy prove the # deployed router and Cilium filters. No Pod, policy, or storage object is changed. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/prove-scoped-package-access.yaml b/.github/workflows/prove-scoped-package-access.yaml index b48700b678..6b5b5b25d2 100644 --- a/.github/workflows/prove-scoped-package-access.yaml +++ b/.github/workflows/prove-scoped-package-access.yaml @@ -1,8 +1,9 @@ +--- name: Prove Scoped Package Access # #3274 is an experiment, not an authentication cutover. Only reviewed main may # use the existing App and Flux pull credentials; PRs run the hermetic tests. -on: +"on": workflow_dispatch: permissions: {} @@ -64,7 +65,10 @@ jobs: with: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: devantler-tech + # zizmor 1.25.0 (MegaLinter v10.1.0) flags `owner` unless a singular `repository` + # input exists, which this action does not have; `repositories` scopes the token + # (zizmorcore/zizmor#2011, fixed in zizmor 1.25.1). + owner: devantler-tech # zizmor: ignore[github-app] repositories: wedding-app permission-packages: read diff --git a/.github/workflows/publish-coroot-checkpoint-hotfix.yaml b/.github/workflows/publish-coroot-checkpoint-hotfix.yaml index 30ff1c7da7..534aa8bfe7 100644 --- a/.github/workflows/publish-coroot-checkpoint-hotfix.yaml +++ b/.github/workflows/publish-coroot-checkpoint-hotfix.yaml @@ -1,6 +1,7 @@ +--- name: Publish Coroot Checkpoint Hotfix -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-coroot-node-agent-hotfix.yaml b/.github/workflows/publish-coroot-node-agent-hotfix.yaml index 156ca6869b..3c649d50b9 100644 --- a/.github/workflows/publish-coroot-node-agent-hotfix.yaml +++ b/.github/workflows/publish-coroot-node-agent-hotfix.yaml @@ -1,6 +1,7 @@ +--- name: Publish Coroot Node Agent Hotfix -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-kubescape-node-agent-hotfix.yaml b/.github/workflows/publish-kubescape-node-agent-hotfix.yaml index c673949591..e9ff8173c0 100644 --- a/.github/workflows/publish-kubescape-node-agent-hotfix.yaml +++ b/.github/workflows/publish-kubescape-node-agent-hotfix.yaml @@ -1,6 +1,7 @@ +--- name: Publish Kubescape Node Agent Hotfix -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-kubescape-storage-hotfix.yaml b/.github/workflows/publish-kubescape-storage-hotfix.yaml index 7e33ae71e5..554f5f6191 100644 --- a/.github/workflows/publish-kubescape-storage-hotfix.yaml +++ b/.github/workflows/publish-kubescape-storage-hotfix.yaml @@ -1,6 +1,7 @@ +--- name: Publish Kubescape Storage Hotfix -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-kyverno-background-hotfix.yaml b/.github/workflows/publish-kyverno-background-hotfix.yaml index bcd85248d6..21dcccf356 100644 --- a/.github/workflows/publish-kyverno-background-hotfix.yaml +++ b/.github/workflows/publish-kyverno-background-hotfix.yaml @@ -1,6 +1,7 @@ +--- name: Publish Kyverno Background Hotfix -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-kyverno-migration-runtime.yaml b/.github/workflows/publish-kyverno-migration-runtime.yaml index c3de89ea7d..1b6e9b4500 100644 --- a/.github/workflows/publish-kyverno-migration-runtime.yaml +++ b/.github/workflows/publish-kyverno-migration-runtime.yaml @@ -1,6 +1,7 @@ +--- name: Publish Kyverno Migration Runtime -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-kyverno-reports-hotfix.yaml b/.github/workflows/publish-kyverno-reports-hotfix.yaml index 522e2b58f9..88ce2ce351 100644 --- a/.github/workflows/publish-kyverno-reports-hotfix.yaml +++ b/.github/workflows/publish-kyverno-reports-hotfix.yaml @@ -1,6 +1,7 @@ +--- name: Publish Kyverno Reports Hotfix -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-private-mail-bootstrap.yaml b/.github/workflows/publish-private-mail-bootstrap.yaml index 1f0232c1f6..1426f887b3 100644 --- a/.github/workflows/publish-private-mail-bootstrap.yaml +++ b/.github/workflows/publish-private-mail-bootstrap.yaml @@ -1,6 +1,7 @@ +--- name: Publish Email Bootstrap -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/publish-release-recovery.yaml b/.github/workflows/publish-release-recovery.yaml index 81aaca541b..519409427e 100644 --- a/.github/workflows/publish-release-recovery.yaml +++ b/.github/workflows/publish-release-recovery.yaml @@ -1,3 +1,4 @@ +--- name: 🩹 Publish Release Recovery # Platform-owned recovery for the captured tag-only incidents (#4839). @@ -5,7 +6,7 @@ name: 🩹 Publish Release Recovery # The boundary test and generated offline fixture guard this wrapper. # The rollout switch was retired after both protected recoveries and no-write proof (.github#603). # confirm-publication remains permanent, default-false operator consent. -on: +"on": workflow_dispatch: inputs: confirm-publication: @@ -125,7 +126,10 @@ jobs: with: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: devantler-tech + # zizmor 1.25.0 (MegaLinter v10.1.0) flags `owner` unless a singular `repository` + # input exists, which this action does not have; `repositories` scopes the token + # (zizmorcore/zizmor#2011, fixed in zizmor 1.25.1). + owner: devantler-tech # zizmor: ignore[github-app] repositories: ${{ steps.admit.outputs.name }} permission-contents: read permission-actions: read @@ -200,7 +204,8 @@ jobs: with: client-id: ${{ vars.APP_CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - owner: devantler-tech + # Same zizmor 1.25.0 false positive as the read-only token above. + owner: devantler-tech # zizmor: ignore[github-app] repositories: ${{ steps.admit.outputs.name }} permission-contents: write permission-actions: read diff --git a/.github/workflows/publish-unsigned-probe-image.yaml b/.github/workflows/publish-unsigned-probe-image.yaml index 52d483d3f4..9cbb10b997 100644 --- a/.github/workflows/publish-unsigned-probe-image.yaml +++ b/.github/workflows/publish-unsigned-probe-image.yaml @@ -1,3 +1,4 @@ +--- name: Publish Unsigned Probe Image # Publish the deliberately UNSIGNED negative control that @@ -56,7 +57,7 @@ name: Publish Unsigned Probe Image # object, and the image is used by nothing. Old tags can be deleted from the # package page at any time. -on: +"on": workflow_dispatch: permissions: {} diff --git a/.github/workflows/read-actual-budget-userns-maps.yaml b/.github/workflows/read-actual-budget-userns-maps.yaml index b0d057ce78..3f16469d25 100644 --- a/.github/workflows/read-actual-budget-userns-maps.yaml +++ b/.github/workflows/read-actual-budget-userns-maps.yaml @@ -1,3 +1,4 @@ +--- name: Read Actual Budget User Namespace Maps # Obtain the ONE observation #3604 is missing: that both live Actual Budget containers really run @@ -32,7 +33,7 @@ name: Read Actual Budget User Namespace Maps # text. The endpoint helper's stdout is discarded. Its stderr is kept: it carries the command # that masks the address, and on failure the helper's own diagnostics, which name no address. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/read-cilium-api-limiter.yaml b/.github/workflows/read-cilium-api-limiter.yaml index 31de30331d..ffd9aaf772 100644 --- a/.github/workflows/read-cilium-api-limiter.yaml +++ b/.github/workflows/read-cilium-api-limiter.yaml @@ -1,3 +1,4 @@ +--- name: Read Cilium API Limiter # Finite read-only evidence for #4329. The runner starts only the fixed `cilium-dbg metrics @@ -5,7 +6,7 @@ name: Read Cilium API Limiter # KUBE_CONFIG has the established admin@prod pipeline identity; the agent's reader cannot # reach this surface. Reviewed main code, fixed targets/command, a typed confirmation and # sanitized output bound this job. No raw metrics, pod identities or kubectl errors are uploaded. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/read-crossplane-alert-recovery.yaml b/.github/workflows/read-crossplane-alert-recovery.yaml index df9d2e7742..105e04b437 100644 --- a/.github/workflows/read-crossplane-alert-recovery.yaml +++ b/.github/workflows/read-crossplane-alert-recovery.yaml @@ -1,6 +1,7 @@ +--- name: Read Crossplane Alert Recovery -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/read-email-ingress-source.yaml b/.github/workflows/read-email-ingress-source.yaml index 6d4dab22ca..520c859485 100644 --- a/.github/workflows/read-email-ingress-source.yaml +++ b/.github/workflows/read-email-ingress-source.yaml @@ -1,6 +1,7 @@ +--- name: Read Email Ingress Source -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/read-email-probe-state.yaml b/.github/workflows/read-email-probe-state.yaml index b41dd73d30..3cebfecfc3 100644 --- a/.github/workflows/read-email-probe-state.yaml +++ b/.github/workflows/read-email-probe-state.yaml @@ -1,6 +1,7 @@ +--- name: Read Email Probe State -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/read-email-startup-state.yaml b/.github/workflows/read-email-startup-state.yaml index a0936dc5a2..85dd49624d 100644 --- a/.github/workflows/read-email-startup-state.yaml +++ b/.github/workflows/read-email-startup-state.yaml @@ -1,6 +1,7 @@ +--- name: Read Email Startup State -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/recover-retained-wedding-standby.yaml b/.github/workflows/recover-retained-wedding-standby.yaml index e8d92a143f..4eaa77c4d5 100644 --- a/.github/workflows/recover-retained-wedding-standby.yaml +++ b/.github/workflows/recover-retained-wedding-standby.yaml @@ -1,6 +1,7 @@ +--- name: Recover Retained Wedding Standby -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/refresh-email-recovery-backup.yaml b/.github/workflows/refresh-email-recovery-backup.yaml index af378b23a4..08625748ec 100644 --- a/.github/workflows/refresh-email-recovery-backup.yaml +++ b/.github/workflows/refresh-email-recovery-backup.yaml @@ -1,3 +1,4 @@ +--- name: Refresh Email Recovery Backup "on": diff --git a/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml b/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml index 1242eba9db..579c9c7244 100644 --- a/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml +++ b/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml @@ -1,3 +1,4 @@ +--- name: Regenerate Approved Publish Revisions # Regenerate the approved set and its four consumer matchers together. Every @@ -20,7 +21,7 @@ name: Regenerate Approved Publish Revisions # request — never the installation's blanket grant. A pull request opened with # GITHUB_TOKEN would also never trigger this repository's required CI. -on: +"on": workflow_dispatch: schedule: - cron: "20 6 * * *" # Daily at 06:20 UTC, after the 06:00 agent-skills update @@ -75,7 +76,10 @@ jobs: # the contract test reads that file and fails if one is added there and not # here. READ ONLY: contents for each consumer's cd.yaml, actions for the # `📦 CD` run that signed the applied tag. Nothing else. - owner: ${{ github.repository_owner }} + # zizmor 1.25.0 (MegaLinter v10.1.0) flags `owner` unless a singular `repository` + # input exists, which this action does not have; `repositories` scopes the token + # (zizmorcore/zizmor#2011, fixed in zizmor 1.25.1). + owner: ${{ github.repository_owner }} # zizmor: ignore[github-app] repositories: | .github ascoachingogvaner @@ -94,8 +98,9 @@ jobs: private-key: ${{ secrets.APP_PRIVATE_KEY }} # THIS repository only: the checkout, the branch push and the pull request # all happen here. Scoping the write token to one repository is what keeps - # the consumer half above read-only. - owner: ${{ github.repository_owner }} + # the consumer half above read-only. The zizmor 1.25.0 false positive is the + # one described on the consumer-read token above. + owner: ${{ github.repository_owner }} # zizmor: ignore[github-app] repositories: | platform permission-contents: write @@ -203,16 +208,16 @@ jobs: run: ./scripts/guard-publish-workflow-approved-revisions.sh - name: 📦 Open or update the regeneration pull request - # DELIBERATELY NOT gated on `moved`. When a consumer's rollout is reverted the - # set returns to what is committed, `moved` is false, and a gated step would - # skip — leaving the pull request opened by the earlier run standing with the - # superseded set, still mergeable, after this workflow has established it is no - # longer current. Running unconditionally lets the action reconcile that: with - # nothing staged there is nothing to propose, and `delete-branch` retires the - # fixed branch rather than leaving a superseded draft standing. - # - # A failure still stops it: there is no `if: always()`, so a red generator or a - # red guard means this step never runs, which the contract test pins. + # DELIBERATELY NOT gated on `moved`. When a consumer's rollout is reverted the + # set returns to what is committed, `moved` is false, and a gated step would + # skip — leaving the pull request opened by the earlier run standing with the + # superseded set, still mergeable, after this workflow has established it is no + # longer current. Running unconditionally lets the action reconcile that: with + # nothing staged there is nothing to propose, and `delete-branch` retires the + # fixed branch rather than leaving a superseded draft standing. + # + # A failure still stops it: there is no `if: always()`, so a red generator or a + # red guard means this step never runs, which the contract test pins. uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: # Exactly the set and matcher outputs. `add-paths` is the second fence behind diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 775e17c9f8..248ac34694 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,5 +1,6 @@ +--- name: Release -on: +"on": workflow_dispatch: push: branches: diff --git a/.github/workflows/remove-wedding-shared-backups.yaml b/.github/workflows/remove-wedding-shared-backups.yaml index 2a9930547b..2993d1a80a 100644 --- a/.github/workflows/remove-wedding-shared-backups.yaml +++ b/.github/workflows/remove-wedding-shared-backups.yaml @@ -1,8 +1,9 @@ +--- name: Remove Retired Wedding Shared Backups # One approved retirement (#4481), dormant until an explicit main dispatch. # Source CI and the merge queue never execute this workflow. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/replace-wedding-standby.yaml b/.github/workflows/replace-wedding-standby.yaml index fbd3377c18..0ef281640b 100644 --- a/.github/workflows/replace-wedding-standby.yaml +++ b/.github/workflows/replace-wedding-standby.yaml @@ -1,6 +1,7 @@ +--- name: Replace Failed Wedding Standby -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/restore-stranded-prod.yaml b/.github/workflows/restore-stranded-prod.yaml index 7fd1f4cfb8..36d6eec4bc 100644 --- a/.github/workflows/restore-stranded-prod.yaml +++ b/.github/workflows/restore-stranded-prod.yaml @@ -1,3 +1,4 @@ +--- name: Restore Stranded Prod # Production can keep an artifact that never reached main (#4186): a heal decided outside @@ -11,7 +12,7 @@ name: Restore Stranded Prod # prod-deploy lock, a DIVERGED prod with an empty merge queue and no unfinished merge-group # run can only be stranded, so it redeploys main through the manual CD workflow, which # carries every production gate. Any failed read redeploys nothing and fails the run. -on: +"on": workflow_dispatch: schedule: - cron: "40 * * * *" # Hourly diff --git a/.github/workflows/scan-go-vulnerabilities.yaml b/.github/workflows/scan-go-vulnerabilities.yaml index 27d5ac9465..091fbefcf2 100644 --- a/.github/workflows/scan-go-vulnerabilities.yaml +++ b/.github/workflows/scan-go-vulnerabilities.yaml @@ -1,3 +1,4 @@ +--- name: Scan Go Vulnerabilities # Reports advisories the platform's Go modules actually reach (#4961). A pull request that changes @@ -16,7 +17,7 @@ name: Scan Go Vulnerabilities # The scan itself is the organization's reviewed govulncheck action, the same scanner and # allowlist format the shared Go validation uses. Resolve a finding with # docs/go-vulnerability-scan.md. -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/stale-issues.yaml b/.github/workflows/stale-issues.yaml index 39afe14e75..aeacac595e 100644 --- a/.github/workflows/stale-issues.yaml +++ b/.github/workflows/stale-issues.yaml @@ -1,11 +1,13 @@ --- name: 🗂️ Stale Issue Triage -on: +"on": pull_request: paths: [.github/workflows/stale-issues.yaml] schedule: - cron: "33 6 * * *" + # checkov:skip=CKV_GHA_7:Not a build. `dry-run` (default true) only lets a manual run preview + # issue triage; the workflow produces no artifact for an input to influence. workflow_dispatch: inputs: dry-run: diff --git a/.github/workflows/todos.yaml b/.github/workflows/todos.yaml index 29bb6ed3f8..f4fcc809f6 100644 --- a/.github/workflows/todos.yaml +++ b/.github/workflows/todos.yaml @@ -1,5 +1,6 @@ +--- name: "TODOs" -on: +"on": workflow_dispatch: push: branches: [main] diff --git a/.github/workflows/update-skills.yaml b/.github/workflows/update-skills.yaml index 740c640338..46a14c0c24 100644 --- a/.github/workflows/update-skills.yaml +++ b/.github/workflows/update-skills.yaml @@ -1,5 +1,6 @@ +--- name: Update Agent Skills -on: +"on": workflow_dispatch: schedule: - cron: "0 6 * * *" # Daily at 06:00 UTC diff --git a/.github/workflows/validate-coroot-security-progress.yaml b/.github/workflows/validate-coroot-security-progress.yaml index c29d28bd5a..c8101198f7 100644 --- a/.github/workflows/validate-coroot-security-progress.yaml +++ b/.github/workflows/validate-coroot-security-progress.yaml @@ -1,6 +1,7 @@ +--- name: Validate Coroot Security Progress -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/validate-image-verifier-liveness.yaml b/.github/workflows/validate-image-verifier-liveness.yaml index 62d71186ab..7405ad87eb 100644 --- a/.github/workflows/validate-image-verifier-liveness.yaml +++ b/.github/workflows/validate-image-verifier-liveness.yaml @@ -1,3 +1,4 @@ +--- name: Validate Image Verifier Liveness # Assert that node-level image verification can enforce AT ALL. @@ -43,7 +44,7 @@ name: Validate Image Verifier Liveness # the scheduled run, or losing `environment: prod` fails the PR. This workflow # therefore carries only the checks that need the fleet. -on: +"on": schedule: - cron: "43 4 * * *" # Daily at 04:43 UTC — see WHEN above workflow_dispatch: diff --git a/.github/workflows/validate-main.yaml b/.github/workflows/validate-main.yaml index f3658ce777..ecf15784db 100644 --- a/.github/workflows/validate-main.yaml +++ b/.github/workflows/validate-main.yaml @@ -1,3 +1,4 @@ +--- name: Validate Main # ci.yaml runs the authorization gate on `pull_request` and `merge_group` only, @@ -15,7 +16,7 @@ name: Validate Main # authorization surface, which a change anywhere in the overlays (or in the # validator itself) can move; a paths filter here would recreate a narrower # version of the same blind spot. -on: +"on": push: branches: [main] # Validate a trusted prospective revision with the original read-only jobs. diff --git a/.github/workflows/verify-alert-publisher-app.yaml b/.github/workflows/verify-alert-publisher-app.yaml index b307d8faa5..62ddcb47bb 100644 --- a/.github/workflows/verify-alert-publisher-app.yaml +++ b/.github/workflows/verify-alert-publisher-app.yaml @@ -1,9 +1,12 @@ +--- name: Verify Alert Publisher App # Coverage bootstrap for #5291/#5294. The publisher stays dormant. This reads # GitHub metadata, mints one repository-scoped token, then revokes that token. # It never runs the publisher state machine or creates an incident/ask. -on: +"on": + # checkov:skip=CKV_GHA_7:Not a build. `confirm` is the typed interlock for this read-only + # proof and nothing is built or published, so removing the input would only remove the guard. workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/verify-arc-app-identity.yaml b/.github/workflows/verify-arc-app-identity.yaml index be871500da..6b3ce8ba56 100644 --- a/.github/workflows/verify-arc-app-identity.yaml +++ b/.github/workflows/verify-arc-app-identity.yaml @@ -1,7 +1,8 @@ +--- name: Verify ARC App Identity # Production proof is manual only. PRs run credential-free fixtures. -on: +"on": pull_request: branches: [main] paths: diff --git a/.github/workflows/verify-arc-runner-group-capability.yaml b/.github/workflows/verify-arc-runner-group-capability.yaml index 46f95cd7de..f90916f889 100644 --- a/.github/workflows/verify-arc-runner-group-capability.yaml +++ b/.github/workflows/verify-arc-runner-group-capability.yaml @@ -1,6 +1,7 @@ +--- name: Verify ARC Runner Group Capability -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/verify-arc-transport.yaml b/.github/workflows/verify-arc-transport.yaml index b841d99b41..622f6f0cd8 100644 --- a/.github/workflows/verify-arc-transport.yaml +++ b/.github/workflows/verify-arc-transport.yaml @@ -1,6 +1,7 @@ +--- name: Verify ARC Credential Transport -on: +"on": workflow_dispatch: permissions: {} diff --git a/.github/workflows/verify-email-credential-custody.yaml b/.github/workflows/verify-email-credential-custody.yaml index c37f58c59f..5f6eabe0c1 100644 --- a/.github/workflows/verify-email-credential-custody.yaml +++ b/.github/workflows/verify-email-credential-custody.yaml @@ -1,3 +1,4 @@ +--- name: Verify Email Credential Custody "on": diff --git a/.github/workflows/verify-email-data-recovery.yaml b/.github/workflows/verify-email-data-recovery.yaml index a2f3589db7..e1baa82993 100644 --- a/.github/workflows/verify-email-data-recovery.yaml +++ b/.github/workflows/verify-email-data-recovery.yaml @@ -1,3 +1,4 @@ +--- name: Verify Email Mailbox Data Recovery "on": diff --git a/.github/workflows/verify-email-provider-smtp.yaml b/.github/workflows/verify-email-provider-smtp.yaml index bbca3222db..0e2c2ac33f 100644 --- a/.github/workflows/verify-email-provider-smtp.yaml +++ b/.github/workflows/verify-email-provider-smtp.yaml @@ -1,3 +1,4 @@ +--- name: Verify Email Provider SMTP "on": diff --git a/.github/workflows/verify-email-storage-canary.yaml b/.github/workflows/verify-email-storage-canary.yaml index 1b2737169e..3cc84fa0ec 100644 --- a/.github/workflows/verify-email-storage-canary.yaml +++ b/.github/workflows/verify-email-storage-canary.yaml @@ -1,3 +1,4 @@ +--- name: Verify Email Storage Canary "on": diff --git a/.github/workflows/verify-email-storage-key-recovery.yaml b/.github/workflows/verify-email-storage-key-recovery.yaml index 7fc8959e36..b04433ebc9 100644 --- a/.github/workflows/verify-email-storage-key-recovery.yaml +++ b/.github/workflows/verify-email-storage-key-recovery.yaml @@ -1,3 +1,4 @@ +--- name: Verify Email Storage Key Recovery "on": diff --git a/.github/workflows/verify-heartbeat-cleanup.yaml b/.github/workflows/verify-heartbeat-cleanup.yaml index 9371ba3c34..47529b64c7 100644 --- a/.github/workflows/verify-heartbeat-cleanup.yaml +++ b/.github/workflows/verify-heartbeat-cleanup.yaml @@ -1,3 +1,4 @@ +--- name: Verify Heartbeat Cleanup "on": diff --git a/.github/workflows/verify-openbao-backup-recovery.yaml b/.github/workflows/verify-openbao-backup-recovery.yaml index f6a1220352..7f12fab46e 100644 --- a/.github/workflows/verify-openbao-backup-recovery.yaml +++ b/.github/workflows/verify-openbao-backup-recovery.yaml @@ -1,3 +1,4 @@ +--- name: Verify OpenBao Backup Recovery "on": diff --git a/.github/workflows/verify-private-mail-storage-recovery.yaml b/.github/workflows/verify-private-mail-storage-recovery.yaml index 861c808bb0..1ab76c875b 100644 --- a/.github/workflows/verify-private-mail-storage-recovery.yaml +++ b/.github/workflows/verify-private-mail-storage-recovery.yaml @@ -1,3 +1,4 @@ +--- name: Verify Email Storage Recovery "on": diff --git a/.github/workflows/verify-wedding-backup-bootstrap.yaml b/.github/workflows/verify-wedding-backup-bootstrap.yaml index a3a11d7e73..f20027e87d 100644 --- a/.github/workflows/verify-wedding-backup-bootstrap.yaml +++ b/.github/workflows/verify-wedding-backup-bootstrap.yaml @@ -1,7 +1,8 @@ +--- name: Verify Wedding Backup Bootstrap # A disposable fixture on the existing cluster; never scheduled or PR-triggered. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/verify-wedding-backup-cutover.yaml b/.github/workflows/verify-wedding-backup-cutover.yaml index 8a3844af6d..a78a521f76 100644 --- a/.github/workflows/verify-wedding-backup-cutover.yaml +++ b/.github/workflows/verify-wedding-backup-cutover.yaml @@ -1,8 +1,9 @@ +--- name: Verify Wedding Backup Cutover # Dormant operator proof: create one fresh online backup only after the reviewed # dedicated archive is active. Catalogue and isolated restore proofs are separate. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/verify-wedding-backup-denial.yaml b/.github/workflows/verify-wedding-backup-denial.yaml index 58188450a1..0fee268f7a 100644 --- a/.github/workflows/verify-wedding-backup-denial.yaml +++ b/.github/workflows/verify-wedding-backup-denial.yaml @@ -1,3 +1,4 @@ +--- name: Verify Wedding Backup Denial # Prove that the dedicated Wedding backup credential is refused by the shared @@ -16,7 +17,7 @@ name: Verify Wedding Backup Denial # is a run-owned probe object; unless that write was refused, the proof removes # the probe object with the shared credential and fails. Beyond that removal, # the shared credential only lists. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/verify-wedding-dedicated-restore.yaml b/.github/workflows/verify-wedding-dedicated-restore.yaml index 6cd181c784..1d6fef2ce8 100644 --- a/.github/workflows/verify-wedding-dedicated-restore.yaml +++ b/.github/workflows/verify-wedding-dedicated-restore.yaml @@ -1,8 +1,9 @@ +--- name: Verify Wedding Dedicated Restore # An operator proof, never a PR-triggered production mutation. It preserves the # live database and deletes only the temporary namespace owned by this run. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/verify-wedding-fence.yaml b/.github/workflows/verify-wedding-fence.yaml index 3d5a77c3db..1b903c6a78 100644 --- a/.github/workflows/verify-wedding-fence.yaml +++ b/.github/workflows/verify-wedding-fence.yaml @@ -1,6 +1,7 @@ +--- name: Verify Retained Wedding Fence -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.github/workflows/verify-wedding-shared-backup-coverage.yaml b/.github/workflows/verify-wedding-shared-backup-coverage.yaml index ca07736c42..20443dae2b 100644 --- a/.github/workflows/verify-wedding-shared-backup-coverage.yaml +++ b/.github/workflows/verify-wedding-shared-backup-coverage.yaml @@ -1,3 +1,4 @@ +--- name: Verify Wedding Shared Backup Coverage # Prove that the dedicated Wedding backup bucket covers everything the stale copy @@ -13,7 +14,7 @@ name: Verify Wedding Shared Backup Coverage # # READ ONLY. The proof lists both buckets and reads the objects it has to hash. # It never writes to or deletes from either bucket. -on: +"on": workflow_dispatch: inputs: confirm: diff --git a/.mega-linter.yml b/.mega-linter.yml index 8bb226595c..9352bda2db 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -1,3 +1,4 @@ +--- # MegaLinter configuration for the platform repository. # # WHY THIS FILE EXISTS diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..6ba331c022 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# talos/factory-schematic.yaml is the canonical Talos Image Factory schematic byte for byte: its +# sha256 IS the schematic ID registered before every deploy and expected on every node. prettier +# would re-indent it from four spaces to two, and MegaLinter's APPLY_FIXES would commit that as a +# different schematic ID. Its own contract tests check its content; no formatter may rewrite it. +talos/factory-schematic.yaml diff --git a/.sops.yaml b/.sops.yaml index 96843e27a4..6727d384ac 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -1,3 +1,4 @@ +--- creation_rules: - path_regex: ^k8s\/clusters\/local\/.+\.enc\.ya?ml$ encrypted_regex: ^(data|stringData)$ diff --git a/.trivy/data/trusted-registries.yaml b/.trivy/data/trusted-registries.yaml index 5b8c81c5f0..f670ffd06f 100644 --- a/.trivy/data/trusted-registries.yaml +++ b/.trivy/data/trusted-registries.yaml @@ -1,3 +1,4 @@ +--- # Registries this repository is allowed to pull container images from. # # WHY THIS FILE EXISTS diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 3eafd7145a..8c022d08e5 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -1,3 +1,4 @@ +--- # Trivy misconfiguration checks skipped on this repository, with the reason each one is skipped. # # The bar here is the same one .mega-linter.yml applies: a skip names a technical reason that diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000000..34b74dba74 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,23 @@ +--- +# yamllint rules for MegaLinter. MegaLinter reads a repository-root .yamllint.yml in place of +# its bundled default, so everything except the document-start entry is MegaLinter v10.1.0's +# TEMPLATES/.yamllint.yml, unchanged. Keep it in step with that template when MegaLinter moves. +extends: default +rules: + braces: + max-spaces-inside: 1 + new-lines: + level: warning + type: unix + line-length: + max: 500 + comments: + min-spaces-from-content: 1 # Used to follow prettier standard: https://github.com/prettier/prettier/pull/10926 + document-start: + # One file, one rule. talos/factory-schematic.yaml is the canonical Talos Image Factory + # schematic byte for byte: its sha256 IS the schematic ID that + # scripts/register-talos-factory-schematic.sh registers before every deploy and + # scripts/verify-talos-schematic-readback.sh expects on every node. A `---` line would + # change that ID, so this file cannot carry the marker every other YAML file here has. + ignore: | + talos/factory-schematic.yaml diff --git a/scripts/generate-publish-workflow-approved-revisions.sh b/scripts/generate-publish-workflow-approved-revisions.sh index 3980e83125..8e09a6fb39 100755 --- a/scripts/generate-publish-workflow-approved-revisions.sh +++ b/scripts/generate-publish-workflow-approved-revisions.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# generate-publish-workflow-approved-revisions.sh — derive, per consumer of the shared +# generate-publish-workflow-approved-revisions.sh — derive, per consumer of the shared # publish workflows, the two revisions its OCIRepository cosign matcher must accept, and # commit them as a reviewable data file (#3550, first child of #3308). # @@ -14,13 +14,13 @@ # pattern form (APPROVED_REVISIONS_ENFORCE) flips with the narrowing itself. # # THE TWO INPUTS -# applied — the artifact revision Flux has verified and applied, read from the consumer's +# applied — the artifact revision Flux has verified and applied, read from the consumer's # OCIRepository `status.artifact.revision` (`@sha256:`) on the # cluster named by PUBLISH_KUBE_CONTEXT; the revision that SIGNED it is the shared -# workflow revision recorded on the `🚀 CD` run that published exactly that tag +# workflow revision recorded on the `🚀 CD` run that published exactly that tag # (`referenced_workflows[].sha`), which is the same fact the OIDC # `job_workflow_ref` claim is minted from and needs no package read. -# pin — `.jobs[].uses` of the consumer's cd.yaml at its default branch, via the report's +# pin — `.jobs[].uses` of the consumer's cd.yaml at its default branch, via the report's # `pin_at_ref`. # For canonical publications, these current observations retain the full # repository/workflow identity in publish-workflow-approved-revisions.identities.tsv. @@ -45,8 +45,8 @@ # guard can diff the committed file against a fresh run. # # SEAMS (tests substitute these; production leaves them unset) -# APPROVED_REVISION_OBSERVER → "\t\t" -# APPROVED_REVISION_PIN_RESOLVER → "" +# APPROVED_REVISION_OBSERVER → "\t\t" +# APPROVED_REVISION_PIN_RESOLVER → "" # APPROVED_REVISION_RELEASE_RESOLVER -> "" # APPROVED_REVISIONS_FILE output path (default scripts/publish-workflow-approved-revisions.tsv) # APPROVED_REVISIONS_OBSERVED_ON YYYY-MM-DD stamped on rows whose tuple changed (default: today, UTC) @@ -167,7 +167,7 @@ applied_revision() { # The git tag whose publish produced one registry tag. The shared workflows strip a leading # `v` and spell `+` as `_`, so the translation is not invertible: BOTH `v1.2.3` and `1.2.3` # would publish as `1.2.3`. Exactly one existing git tag may translate to the applied tag, -# or the signer cannot be attributed to one commit — the same refusal the report makes. +# or the signer cannot be attributed to one commit — the same refusal the report makes. git_tag_for_registry_tag() { local repo="$1" registry_tag="$2" tags matches count tags="$(gh_retry api --paginate "repos/devantler-tech/${repo}/tags?per_page=100" --jq '.[].name')" || return 1 diff --git a/scripts/tests/crossplane-egress-policy-rules.yaml b/scripts/tests/crossplane-egress-policy-rules.yaml index 0aff5c72df..37a6089650 100644 --- a/scripts/tests/crossplane-egress-policy-rules.yaml +++ b/scripts/tests/crossplane-egress-policy-rules.yaml @@ -1,3 +1,4 @@ +--- rules: # This warning is consumed by the shell harness. It proves KSail rendered the # Crossplane chart before the policy assertion below is accepted as evidence. @@ -699,7 +700,14 @@ rules: type(object.spec.mutations[0].jsonPatch.expression) == string && object.spec.mutations[0].jsonPatch.expression.matches( '^[^"\\x5c]*("[^"\\x5c\\s]*"[^"\\x5c]*)*$') && - [{"label":"helm.toolkit.fluxcd.io/helm-test","conditionName":"tests-enabled-on-incoming-object","conditionExpression":"has(object.metadata.labels) && 'helm.toolkit.fluxcd.io/helm-test' in object.metadata.labels && object.metadata.labels['helm.toolkit.fluxcd.io/helm-test'] == 'enabled'","patch":"(has(object.spec.test)&&object.spec.test!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/test\",value:{}}])+[JSONPatch{op:\"add\",path:\"/spec/test/enable\",value:true}]"},{"label":"helm.toolkit.fluxcd.io/remediation","conditionName":"remediation-enabled-on-incoming-object","conditionExpression":"has(object.metadata.labels) && 'helm.toolkit.fluxcd.io/remediation' in object.metadata.labels && object.metadata.labels['helm.toolkit.fluxcd.io/remediation'] == 'enabled'","patch":"(has(object.spec.install)&&object.spec.install!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/install\",value:{}}])+(has(object.spec.install)&&object.spec.install!=null&&has(object.spec.install.remediation)&&object.spec.install.remediation!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/install/remediation\",value:{}}])+[JSONPatch{op:\"add\",path:\"/spec/install/remediation/retries\",value:3},JSONPatch{op:\"add\",path:\"/spec/install/remediation/remediateLastFailure\",value:true}]+(has(object.spec.upgrade)&&object.spec.upgrade!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/upgrade\",value:{}}])+(has(object.spec.upgrade)&&object.spec.upgrade!=null&&has(object.spec.upgrade.remediation)&&object.spec.upgrade.remediation!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/upgrade/remediation\",value:{}}])+[JSONPatch{op:\"add\",path:\"/spec/upgrade/remediation/retries\",value:3},JSONPatch{op:\"add\",path:\"/spec/upgrade/remediation/remediateLastFailure\",value:true}]"}].exists(approved, + [{"label":"helm.toolkit.fluxcd.io/helm-test", + "conditionName":"tests-enabled-on-incoming-object", + "conditionExpression":"has(object.metadata.labels) && 'helm.toolkit.fluxcd.io/helm-test' in object.metadata.labels && object.metadata.labels['helm.toolkit.fluxcd.io/helm-test'] == 'enabled'", + "patch":"(has(object.spec.test)&&object.spec.test!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/test\",value:{}}])+[JSONPatch{op:\"add\",path:\"/spec/test/enable\",value:true}]"}, + {"label":"helm.toolkit.fluxcd.io/remediation", + "conditionName":"remediation-enabled-on-incoming-object", + "conditionExpression":"has(object.metadata.labels) && 'helm.toolkit.fluxcd.io/remediation' in object.metadata.labels && object.metadata.labels['helm.toolkit.fluxcd.io/remediation'] == 'enabled'", + "patch":"(has(object.spec.install)&&object.spec.install!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/install\",value:{}}])+(has(object.spec.install)&&object.spec.install!=null&&has(object.spec.install.remediation)&&object.spec.install.remediation!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/install/remediation\",value:{}}])+[JSONPatch{op:\"add\",path:\"/spec/install/remediation/retries\",value:3},JSONPatch{op:\"add\",path:\"/spec/install/remediation/remediateLastFailure\",value:true}]+(has(object.spec.upgrade)&&object.spec.upgrade!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/upgrade\",value:{}}])+(has(object.spec.upgrade)&&object.spec.upgrade!=null&&has(object.spec.upgrade.remediation)&&object.spec.upgrade.remediation!=null?[]:[JSONPatch{op:\"add\",path:\"/spec/upgrade/remediation\",value:{}}])+[JSONPatch{op:\"add\",path:\"/spec/upgrade/remediation/retries\",value:3},JSONPatch{op:\"add\",path:\"/spec/upgrade/remediation/remediateLastFailure\",value:true}]"}].exists(approved, object.spec.mutations[0].jsonPatch.expression.replace(' ', '').replace('\n', '').replace('\t', '').replace('\r', '') == approved.patch && object.spec == { 'failurePolicy': 'Fail', @@ -805,11 +813,11 @@ rules: '^[^"\\x5c]*("[^"\\x5c\\s]*"[^"\\x5c]*)*$') && ((object.spec.matchConstraints.resourceRules[0].resources == ['deployments'] && object.spec.generate[0].expression.replace(' ', '').replace('\n', '').replace('\t', '').replace('\r', '') == - 'generator.Apply(string(object.metadata.namespace),[{"apiVersion":dyn("autoscaling.k8s.io/v1"),"kind":dyn("VerticalPodAutoscaler"),"metadata":dyn({"name":dyn(string(object.metadata.name)+"-deployment"),"namespace":dyn(string(object.metadata.namespace)),"ownerReferences":dyn([{"apiVersion":"apps/v1","kind":"Deployment","name":string(object.metadata.name),"uid":string([object.metadata.uid].filter(uid,type(uid)==string&&size(uid)>0)[0])}])}),"spec":dyn({"targetRef":dyn({"apiVersion":"apps/v1","kind":"Deployment","name":string(object.metadata.name)}),"updatePolicy":dyn({"updateMode":"InPlaceOrRecreate"}),"resourcePolicy":dyn({"containerPolicies":[{"containerName":dyn("*"),"controlledResources":dyn(["cpu","memory"]),"controlledValues":dyn("RequestsAndLimits"),"minAllowed":dyn({"cpu":"50m","memory":"64Mi"}),"maxAllowed":dyn({"cpu":"3","memory":"6Gi"})}]})})}])') || - (object.spec.matchConstraints.resourceRules[0].resources == ['daemonsets'] && + 'generator.Apply(string(object.metadata.namespace),[{"apiVersion":dyn("autoscaling.k8s.io/v1"),"kind":dyn("VerticalPodAutoscaler"),"metadata":dyn({"name":dyn(string(object.metadata.name)+"-deployment"),"namespace":dyn(string(object.metadata.namespace)),"ownerReferences":dyn([{"apiVersion":"apps/v1","kind":"Deployment","name":string(object.metadata.name),"uid":string([object.metadata.uid].filter(uid,type(uid)==string&&size(uid)>0)[0])}])}),"spec":dyn({"targetRef":dyn({"apiVersion":"apps/v1","kind":"Deployment","name":string(object.metadata.name)}),"updatePolicy":dyn({"updateMode":"InPlaceOrRecreate"}),"resourcePolicy":dyn({"containerPolicies":[{"containerName":dyn("*"),"controlledResources":dyn(["cpu","memory"]),"controlledValues":dyn("RequestsAndLimits"),"minAllowed":dyn({"cpu":"50m","memory":"64Mi"}),"maxAllowed":dyn({"cpu":"3","memory":"6Gi"})}]})})}])') + || (object.spec.matchConstraints.resourceRules[0].resources == ['daemonsets'] && object.spec.generate[0].expression.replace(' ', '').replace('\n', '').replace('\t', '').replace('\r', '') == - 'generator.Apply(string(object.metadata.namespace),[{"apiVersion":dyn("autoscaling.k8s.io/v1"),"kind":dyn("VerticalPodAutoscaler"),"metadata":dyn({"name":dyn(string(object.metadata.name)+"-daemonset"),"namespace":dyn(string(object.metadata.namespace)),"ownerReferences":dyn([{"apiVersion":"apps/v1","kind":"DaemonSet","name":string(object.metadata.name),"uid":string([object.metadata.uid].filter(uid,type(uid)==string&&size(uid)>0)[0])}])}),"spec":dyn({"targetRef":dyn({"apiVersion":"apps/v1","kind":"DaemonSet","name":string(object.metadata.name)}),"updatePolicy":dyn({"updateMode":"InPlaceOrRecreate"}),"resourcePolicy":dyn({"containerPolicies":[{"containerName":dyn("*"),"controlledResources":dyn(["cpu"]),"controlledValues":dyn("RequestsAndLimits"),"minAllowed":dyn({"cpu":"50m"}),"maxAllowed":dyn({"cpu":"1"})}]})})}])') || - (object.spec.matchConstraints.resourceRules[0].resources == ['statefulsets'] && + 'generator.Apply(string(object.metadata.namespace),[{"apiVersion":dyn("autoscaling.k8s.io/v1"),"kind":dyn("VerticalPodAutoscaler"),"metadata":dyn({"name":dyn(string(object.metadata.name)+"-daemonset"),"namespace":dyn(string(object.metadata.namespace)),"ownerReferences":dyn([{"apiVersion":"apps/v1","kind":"DaemonSet","name":string(object.metadata.name),"uid":string([object.metadata.uid].filter(uid,type(uid)==string&&size(uid)>0)[0])}])}),"spec":dyn({"targetRef":dyn({"apiVersion":"apps/v1","kind":"DaemonSet","name":string(object.metadata.name)}),"updatePolicy":dyn({"updateMode":"InPlaceOrRecreate"}),"resourcePolicy":dyn({"containerPolicies":[{"containerName":dyn("*"),"controlledResources":dyn(["cpu"]),"controlledValues":dyn("RequestsAndLimits"),"minAllowed":dyn({"cpu":"50m"}),"maxAllowed":dyn({"cpu":"1"})}]})})}])') + || (object.spec.matchConstraints.resourceRules[0].resources == ['statefulsets'] && object.spec.generate[0].expression.replace(' ', '').replace('\n', '').replace('\t', '').replace('\r', '') == 'generator.Apply(string(object.metadata.namespace),[{"apiVersion":dyn("autoscaling.k8s.io/v1"),"kind":dyn("VerticalPodAutoscaler"),"metadata":dyn({"name":dyn(string(object.metadata.name)+"-statefulset"),"namespace":dyn(string(object.metadata.namespace)),"ownerReferences":dyn([{"apiVersion":"apps/v1","kind":"StatefulSet","name":string(object.metadata.name),"uid":string([object.metadata.uid].filter(uid,type(uid)==string&&size(uid)>0)[0])}])}),"spec":dyn({"targetRef":dyn({"apiVersion":"apps/v1","kind":"StatefulSet","name":string(object.metadata.name)}),"updatePolicy":dyn({"updateMode":"Initial"}),"resourcePolicy":dyn({"containerPolicies":[{"containerName":dyn("*"),"controlledResources":dyn(["cpu","memory"]),"controlledValues":dyn("RequestsAndLimits"),"minAllowed":dyn({"cpu":"50m","memory":"64Mi"}),"maxAllowed":dyn({"cpu":"3","memory":"6Gi"})}]})})}])'))) message: >- diff --git a/scripts/tests/fixtures/coroot-dns-probe/pod.yaml b/scripts/tests/fixtures/coroot-dns-probe/pod.yaml index b19334f577..14d3cffc3a 100644 --- a/scripts/tests/fixtures/coroot-dns-probe/pod.yaml +++ b/scripts/tests/fixtures/coroot-dns-probe/pod.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Pod metadata: diff --git a/scripts/tests/fixtures/wedding-database-priority/cluster.yaml b/scripts/tests/fixtures/wedding-database-priority/cnpg-cluster.yaml similarity index 98% rename from scripts/tests/fixtures/wedding-database-priority/cluster.yaml rename to scripts/tests/fixtures/wedding-database-priority/cnpg-cluster.yaml index b33763e0a5..542bb87f81 100644 --- a/scripts/tests/fixtures/wedding-database-priority/cluster.yaml +++ b/scripts/tests/fixtures/wedding-database-priority/cnpg-cluster.yaml @@ -1,3 +1,4 @@ +--- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: diff --git a/scripts/tests/isolated-chart-namespace-rules.yaml b/scripts/tests/isolated-chart-namespace-rules.yaml index 43f6bf33c8..b58755d5c3 100644 --- a/scripts/tests/isolated-chart-namespace-rules.yaml +++ b/scripts/tests/isolated-chart-namespace-rules.yaml @@ -1,3 +1,4 @@ +--- rules: - name: restrict-data-product-controller-rendered-child-namespaces # This rule is deliberately path-scoped by its caller to the reviewed diff --git a/scripts/tests/kyverno-admission-vpa-rules.yaml b/scripts/tests/kyverno-admission-vpa-rules.yaml index 212a9e8a98..1617b1b5f9 100644 --- a/scripts/tests/kyverno-admission-vpa-rules.yaml +++ b/scripts/tests/kyverno-admission-vpa-rules.yaml @@ -1,3 +1,4 @@ +--- rules: # A warning is intentional here: the shell harness requires this marker in # KSail's output, proving that the rendered chart still contains the target diff --git a/scripts/tests/production-authorization-rules.yaml b/scripts/tests/production-authorization-rules.yaml index 00377887b9..49cd91e230 100644 --- a/scripts/tests/production-authorization-rules.yaml +++ b/scripts/tests/production-authorization-rules.yaml @@ -1,3 +1,4 @@ +--- rules: # The EKS CI role is assumed through this service account. Bindings that name # it directly, through its canonical user name, or through a containing group diff --git a/scripts/tests/test-hcloud-volume-priority.sh b/scripts/tests/test-hcloud-volume-priority.sh index 0491c75644..52cf3c74dd 100755 --- a/scripts/tests/test-hcloud-volume-priority.sh +++ b/scripts/tests/test-hcloud-volume-priority.sh @@ -51,7 +51,9 @@ done # Cluster; checking the patch source text alone would miss an unwired override. fixture_dir="$(mktemp -d)" trap 'rm -r -- "${fixture_dir}"' EXIT -cp "${script_dir}/fixtures/wedding-database-priority/cluster.yaml" "${fixture_dir}/cluster.yaml" +# The source is not named cluster.yaml: MegaLinter's v8r matches that file name to the +# unrelated RKE cluster schema. The copy keeps the name the kustomization lists. +cp "${script_dir}/fixtures/wedding-database-priority/cnpg-cluster.yaml" "${fixture_dir}/cluster.yaml" printf '%s\n' "${tenant_patches}" >"${fixture_dir}/patches.yaml" PRIORITY_PATCHES="${fixture_dir}/patches.yaml" yq -n '.apiVersion = "kustomize.config.k8s.io/v1beta1" | .kind = "Kustomization" | .resources = ["cluster.yaml"] | .patches = load(strenv(PRIORITY_PATCHES))' \ diff --git a/scripts/tests/test-probe-cilium-externalauth-crossnode.sh b/scripts/tests/test-probe-cilium-externalauth-crossnode.sh index 48bd1765e4..8e7cfe6547 100755 --- a/scripts/tests/test-probe-cilium-externalauth-crossnode.sh +++ b/scripts/tests/test-probe-cilium-externalauth-crossnode.sh @@ -886,7 +886,9 @@ pass 'the rendered in-pod loop runs as a pod would: delivered, complete black-ho wf="$(cat "${workflow}")" output="${wf}" rc=0 -on_block="$(awk '/^on:/{f=1; next} f&&/^[a-z]/{exit} f' "${workflow}")" +# The trigger key is quoted ("on":) so YAML 1.1 readers keep it a string, not `true`. +on_block="$(awk '/^"?on"?:/{f=1; next} f&&/^[a-z]/{exit} f' "${workflow}")" +[ -n "${on_block}" ] || fail 'the workflow trigger block could not be read' grep -Fq 'workflow_dispatch:' <<<"${on_block}" || fail 'the workflow must be dispatch-triggered' if grep -Eq '^[[:space:]]*(schedule|pull_request|pull_request_target|push|merge_group|workflow_run):' <<<"${on_block}"; then fail 'the workflow must have no trigger other than workflow_dispatch' diff --git a/talos-local/cluster/allow-scheduling-on-control-planes.yaml b/talos-local/cluster/allow-scheduling-on-control-planes.yaml index 78ee123cf7..b1ad5e159a 100644 --- a/talos-local/cluster/allow-scheduling-on-control-planes.yaml +++ b/talos-local/cluster/allow-scheduling-on-control-planes.yaml @@ -1,2 +1,3 @@ +--- cluster: allowSchedulingOnControlPlanes: true diff --git a/talos-local/cluster/disable-default-cni-and-kube-proxy.yaml b/talos-local/cluster/disable-default-cni-and-kube-proxy.yaml index 2b7b65d99c..b9b68ed7bc 100644 --- a/talos-local/cluster/disable-default-cni-and-kube-proxy.yaml +++ b/talos-local/cluster/disable-default-cni-and-kube-proxy.yaml @@ -1,3 +1,4 @@ +--- cluster: network: cni: diff --git a/talos-local/cluster/enable-dex-oidc.yaml b/talos-local/cluster/enable-dex-oidc.yaml index 53189e5b30..dca4988762 100644 --- a/talos-local/cluster/enable-dex-oidc.yaml +++ b/talos-local/cluster/enable-dex-oidc.yaml @@ -1,3 +1,4 @@ +--- # OIDC configuration for the kube-apiserver. # Enables Dex as an OIDC identity provider so that tokens issued by Dex # are accepted as valid Kubernetes authentication credentials. diff --git a/talos-local/cluster/rotate-server-certificates.yaml b/talos-local/cluster/rotate-server-certificates.yaml index ad3efa53a9..80635684b0 100644 --- a/talos-local/cluster/rotate-server-certificates.yaml +++ b/talos-local/cluster/rotate-server-certificates.yaml @@ -1,3 +1,4 @@ +--- machine: kubelet: extraArgs: diff --git a/talos/cluster/authenticate-ghcr-pulls.yaml b/talos/cluster/authenticate-ghcr-pulls.yaml index 888227e199..70689fb169 100644 --- a/talos/cluster/authenticate-ghcr-pulls.yaml +++ b/talos/cluster/authenticate-ghcr-pulls.yaml @@ -1,3 +1,4 @@ +--- # Node-level ghcr.io registry auth for first-party image-signature verification. # # talos/cluster/verify-first-party-images.yaml (ImageVerificationConfig) verifies the diff --git a/talos/cluster/block-ingress-by-default.yaml b/talos/cluster/block-ingress-by-default.yaml index f59a7b7233..3fc81d4c38 100644 --- a/talos/cluster/block-ingress-by-default.yaml +++ b/talos/cluster/block-ingress-by-default.yaml @@ -1,3 +1,4 @@ +--- # Block all ingress traffic by default. Explicit allow rules are defined # per node role in control-planes/ and workers/ directories. # diff --git a/talos/cluster/disable-default-cni-and-kube-proxy.yaml b/talos/cluster/disable-default-cni-and-kube-proxy.yaml index 2b7b65d99c..b9b68ed7bc 100644 --- a/talos/cluster/disable-default-cni-and-kube-proxy.yaml +++ b/talos/cluster/disable-default-cni-and-kube-proxy.yaml @@ -1,3 +1,4 @@ +--- cluster: network: cni: diff --git a/talos/cluster/enable-apparmor.yaml b/talos/cluster/enable-apparmor.yaml index c271eac235..a7025e07be 100644 --- a/talos/cluster/enable-apparmor.yaml +++ b/talos/cluster/enable-apparmor.yaml @@ -1,5 +1,11 @@ +--- machine: install: + # KSail folds this native Talos declaration into the Image Factory + # schematic, clears the deprecated rendered extraKernelArgs field, and pins + # grubUseUKICmdline=true so the baked-in arguments take effect. The canonical + # registration body in talos/factory-schematic.yaml is contract-tested + # against this source declaration and registered before production publish. extraKernelArgs: # Keep Talos v1.13.12's CONFIG_LSM list, but select AppArmor before # SELinux: these major LSMs are exclusive, so only the first activates. @@ -9,8 +15,3 @@ machine: # apparmor=1 enables AppArmor explicitly rather than relying on the kernel default. - apparmor=1 - lsm=yama,apparmor,selinux,loadpin,safesetid,integrity,bpf,lockdown,landlock - # KSail folds this native Talos declaration into the Image Factory - # schematic, clears the deprecated rendered extraKernelArgs field, and pins - # grubUseUKICmdline=true so the baked-in arguments take effect. The canonical - # registration body in talos/factory-schematic.yaml is contract-tested - # against this source declaration and registered before production publish. diff --git a/talos/cluster/enable-audit-logging.yaml b/talos/cluster/enable-audit-logging.yaml index ecf1642727..9889f94aad 100644 --- a/talos/cluster/enable-audit-logging.yaml +++ b/talos/cluster/enable-audit-logging.yaml @@ -1,3 +1,4 @@ +--- # Kubernetes API server audit logging. # # Places an audit policy file on disk and configures the API server to diff --git a/talos/cluster/enable-dex-oidc.yaml b/talos/cluster/enable-dex-oidc.yaml index 6c5df200a9..c698a04b49 100644 --- a/talos/cluster/enable-dex-oidc.yaml +++ b/talos/cluster/enable-dex-oidc.yaml @@ -1,3 +1,4 @@ +--- # OIDC configuration for the kube-apiserver on Hetzner clusters. # Enables Dex as an OIDC identity provider so that tokens issued by Dex # are accepted as valid Kubernetes authentication credentials. diff --git a/talos/cluster/enable-user-namespaces.yaml b/talos/cluster/enable-user-namespaces.yaml index ba28e79962..f430721614 100644 --- a/talos/cluster/enable-user-namespaces.yaml +++ b/talos/cluster/enable-user-namespaces.yaml @@ -1,3 +1,4 @@ +--- machine: sysctls: user.max_user_namespaces: "11255" diff --git a/talos/cluster/encrypt-ephemeral-volume.yaml b/talos/cluster/encrypt-ephemeral-volume.yaml index 00b7326d14..d1efb287a8 100644 --- a/talos/cluster/encrypt-ephemeral-volume.yaml +++ b/talos/cluster/encrypt-ephemeral-volume.yaml @@ -1,3 +1,4 @@ +--- # Encrypt the EPHEMERAL partition with a nodeID-derived key (LUKS2). # # nodeID uses the Hetzner VM UUID to derive the encryption key, protecting diff --git a/talos/cluster/encrypt-state-volume.yaml b/talos/cluster/encrypt-state-volume.yaml index bb5d4b35b7..ebeafe9da3 100644 --- a/talos/cluster/encrypt-state-volume.yaml +++ b/talos/cluster/encrypt-state-volume.yaml @@ -1,3 +1,4 @@ +--- # Encrypt the STATE partition with a nodeID-derived key (LUKS2). # # nodeID uses the Hetzner VM UUID to derive the encryption key, protecting diff --git a/talos/cluster/evict-pods-before-oom.yaml b/talos/cluster/evict-pods-before-oom.yaml index 996ff5cfb8..b3b99af38b 100644 --- a/talos/cluster/evict-pods-before-oom.yaml +++ b/talos/cluster/evict-pods-before-oom.yaml @@ -1,3 +1,4 @@ +--- # Kubelet memory-pressure handling — graceful node OOM. # # Out of the box the kubelet only steps in at its tiny default hard floor diff --git a/talos/cluster/gc-terminated-pods-sooner.yaml b/talos/cluster/gc-terminated-pods-sooner.yaml index 850ad42c13..5700fcdb1e 100644 --- a/talos/cluster/gc-terminated-pods-sooner.yaml +++ b/talos/cluster/gc-terminated-pods-sooner.yaml @@ -1,3 +1,4 @@ +--- # Terminated-pod garbage collection (kube-controller-manager PodGC). # # Kubernetes only reaps terminated pods (phase Succeeded/Failed whose owner diff --git a/talos/cluster/gc-unused-images-before-pressure.yaml b/talos/cluster/gc-unused-images-before-pressure.yaml index ce8b369af3..d4e866e18a 100644 --- a/talos/cluster/gc-unused-images-before-pressure.yaml +++ b/talos/cluster/gc-unused-images-before-pressure.yaml @@ -1,3 +1,4 @@ +--- # Keep container image garbage collection ahead of the platform's storage # warning boundary. # diff --git a/talos/cluster/harden-kernel-sysctls.yaml b/talos/cluster/harden-kernel-sysctls.yaml index af6b7a88e1..36edb1c700 100644 --- a/talos/cluster/harden-kernel-sysctls.yaml +++ b/talos/cluster/harden-kernel-sysctls.yaml @@ -1,3 +1,4 @@ +--- machine: sysctls: # Network buffer tuning (Cilium / high-throughput workloads). diff --git a/talos/cluster/mark-ghcr-pull-revision.yaml b/talos/cluster/mark-ghcr-pull-revision.yaml index f9913983b1..1d6a30daac 100644 --- a/talos/cluster/mark-ghcr-pull-revision.yaml +++ b/talos/cluster/mark-ghcr-pull-revision.yaml @@ -1,3 +1,4 @@ +--- # Non-secret rotation marker for the node-level GHCR pull credential. # # KSail redacts RegistryAuthConfig credentials when it fingerprints Talos diff --git a/talos/cluster/pack-pods-onto-busy-nodes.yaml b/talos/cluster/pack-pods-onto-busy-nodes.yaml index 86e4a2ec0d..81166f458a 100644 --- a/talos/cluster/pack-pods-onto-busy-nodes.yaml +++ b/talos/cluster/pack-pods-onto-busy-nodes.yaml @@ -1,3 +1,4 @@ +--- # kube-scheduler scoring: favour fuller nodes that still fit (MostAllocated). # # The descheduler's HighNodeUtilization strategy drains nearly-empty nodes so the diff --git a/talos/cluster/rotate-server-certificates.yaml b/talos/cluster/rotate-server-certificates.yaml index ad3efa53a9..80635684b0 100644 --- a/talos/cluster/rotate-server-certificates.yaml +++ b/talos/cluster/rotate-server-certificates.yaml @@ -1,3 +1,4 @@ +--- machine: kubelet: extraArgs: diff --git a/talos/cluster/use-platform-hostname.yaml b/talos/cluster/use-platform-hostname.yaml index 3729115751..e4d9a441c1 100644 --- a/talos/cluster/use-platform-hostname.yaml +++ b/talos/cluster/use-platform-hostname.yaml @@ -1,3 +1,4 @@ +--- # Use the platform-provided hostname (the Hetzner server name) instead of # Talos's auto-generated stable hostname. # diff --git a/talos/cluster/verify-first-party-images.yaml b/talos/cluster/verify-first-party-images.yaml index 419d05230c..9194d4eb53 100644 --- a/talos/cluster/verify-first-party-images.yaml +++ b/talos/cluster/verify-first-party-images.yaml @@ -1,3 +1,4 @@ +--- # Container image signature verification (Talos 1.13+, ImageVerificationConfig). # # STATUS — Talos evaluates these rules on this cluster. diff --git a/talos/control-planes/allow-internal-node-ingress.yaml b/talos/control-planes/allow-internal-node-ingress.yaml index e8e3d7d85e..e6bacbf3be 100644 --- a/talos/control-planes/allow-internal-node-ingress.yaml +++ b/talos/control-planes/allow-internal-node-ingress.yaml @@ -1,3 +1,4 @@ +--- # Cluster-internal TCP reachable from the node CIDR only on control-plane # nodes. Pairs with cluster/block-ingress-by-default.yaml. # etcd 2379-2380, cilium-health 4240, cilium-mutual-auth 4250, diff --git a/talos/control-planes/allow-internal-nodepod-ingress.yaml b/talos/control-planes/allow-internal-nodepod-ingress.yaml index 0dede6967a..d0c38547d3 100644 --- a/talos/control-planes/allow-internal-nodepod-ingress.yaml +++ b/talos/control-planes/allow-internal-nodepod-ingress.yaml @@ -1,3 +1,4 @@ +--- # Cluster-internal TCP reachable from the node CIDR AND the pod CIDR on # control-plane nodes. Pairs with cluster/block-ingress-by-default.yaml. # The node CIDR covers cross-node scrapes (Cilium masquerades pod->node-IP to diff --git a/talos/control-planes/allow-internal-udp-ingress.yaml b/talos/control-planes/allow-internal-udp-ingress.yaml index 807793658f..70070f4abf 100644 --- a/talos/control-planes/allow-internal-udp-ingress.yaml +++ b/talos/control-planes/allow-internal-udp-ingress.yaml @@ -1,3 +1,4 @@ +--- # Cluster-internal UDP reachable from the node CIDR only on control-plane # nodes. Pairs with cluster/block-ingress-by-default.yaml. # Cilium VXLAN 8472, Cilium WireGuard 51871 diff --git a/talos/control-planes/allow-public-ingress.yaml b/talos/control-planes/allow-public-ingress.yaml index 07191fd45b..da42ae0ed6 100644 --- a/talos/control-planes/allow-public-ingress.yaml +++ b/talos/control-planes/allow-public-ingress.yaml @@ -1,3 +1,4 @@ +--- # World-reachable management endpoints on control-plane nodes: # Kubernetes API (6443) + apid (50000). Pairs with # cluster/block-ingress-by-default.yaml (NetworkDefaultActionConfig: block). diff --git a/talos/control-planes/allow-wireguard-server-ingress.yaml b/talos/control-planes/allow-wireguard-server-ingress.yaml index ce794a5720..972ff9ec0a 100644 --- a/talos/control-planes/allow-wireguard-server-ingress.yaml +++ b/talos/control-planes/allow-wireguard-server-ingress.yaml @@ -1,3 +1,4 @@ +--- # World-reachable WireGuard server (51820/udp). The home UniFi gateway dials in # from a dynamic residential IP, so the source can't be pinned — but WireGuard # never replies to an unauthenticated packet, so an open source is safe (the diff --git a/talos/control-planes/wireguard.yaml b/talos/control-planes/wireguard.yaml index 95942d3b2f..e3d75ab76f 100644 --- a/talos/control-planes/wireguard.yaml +++ b/talos/control-planes/wireguard.yaml @@ -1,3 +1,4 @@ +--- # WireGuard server (wg0) on the control planes — the dial-in endpoint a home # UniFi gateway (and later CI) connects to, so cluster access rides an encrypted # tunnel instead of being exposed publicly. diff --git a/talos/workers/allow-apid-ingress.yaml b/talos/workers/allow-apid-ingress.yaml index e285554950..e609672c82 100644 --- a/talos/workers/allow-apid-ingress.yaml +++ b/talos/workers/allow-apid-ingress.yaml @@ -1,3 +1,4 @@ +--- # apid (50000) open to all on worker nodes — KSail manages workers via # public IPs. Pairs with cluster/block-ingress-by-default.yaml. # Keep the rule COUNT low — consolidate ports into an existing rule when the diff --git a/talos/workers/allow-internal-node-ingress.yaml b/talos/workers/allow-internal-node-ingress.yaml index ee3d4fe968..d85d694120 100644 --- a/talos/workers/allow-internal-node-ingress.yaml +++ b/talos/workers/allow-internal-node-ingress.yaml @@ -1,3 +1,4 @@ +--- # Cluster-internal TCP reachable from the node CIDR only on worker nodes. # Pairs with cluster/block-ingress-by-default.yaml. # cilium-health 4240, cilium-mutual-auth 4250, NodePort range 30000-32767 diff --git a/talos/workers/allow-internal-nodepod-ingress.yaml b/talos/workers/allow-internal-nodepod-ingress.yaml index 527b2b372d..9eeac4812d 100644 --- a/talos/workers/allow-internal-nodepod-ingress.yaml +++ b/talos/workers/allow-internal-nodepod-ingress.yaml @@ -1,3 +1,4 @@ +--- # Cluster-internal TCP reachable from the node CIDR and the pod CIDR on worker # nodes. Pairs with cluster/block-ingress-by-default.yaml. # Hubble peer service 4244, node-exporter 9100 (Prometheus scraping), diff --git a/talos/workers/allow-internal-udp-ingress.yaml b/talos/workers/allow-internal-udp-ingress.yaml index 0bb3564b30..676e795f26 100644 --- a/talos/workers/allow-internal-udp-ingress.yaml +++ b/talos/workers/allow-internal-udp-ingress.yaml @@ -1,3 +1,4 @@ +--- # Cluster-internal UDP reachable from the node CIDR only on worker nodes. # Pairs with cluster/block-ingress-by-default.yaml. # Cilium VXLAN 8472, Cilium WireGuard 51871 diff --git a/talos/workers/enable-longhorn-v2.yaml b/talos/workers/enable-longhorn-v2.yaml index 33f46d1485..c7e9de17e7 100644 --- a/talos/workers/enable-longhorn-v2.yaml +++ b/talos/workers/enable-longhorn-v2.yaml @@ -1,3 +1,4 @@ +--- # Host prerequisites only (platform#5582). Refresh Clients verifies the actual # allocation and restarts kubelet only where huge-page capacity is stale; Longhorn # V2 activation remains a separate, reviewed change. nvme_tcp is also required but diff --git a/talos/workers/label-nodes.yaml b/talos/workers/label-nodes.yaml index 00b6007749..5188b856df 100644 --- a/talos/workers/label-nodes.yaml +++ b/talos/workers/label-nodes.yaml @@ -1,3 +1,4 @@ +--- # Kubelet node-labels for Talos workers. # # All node-labels must be combined in this single file: Talos strategic diff --git a/talos/workers/load-kvm-modules.yaml b/talos/workers/load-kvm-modules.yaml index b4433958db..3b99917e4e 100644 --- a/talos/workers/load-kvm-modules.yaml +++ b/talos/workers/load-kvm-modules.yaml @@ -1,3 +1,4 @@ +--- # KubeVirt requires KVM kernel modules for hardware-accelerated # virtualization on Hetzner workers. machine: diff --git a/talos/workers/mount-longhorn-data.yaml b/talos/workers/mount-longhorn-data.yaml index 0c728fdb83..e9aece7737 100644 --- a/talos/workers/mount-longhorn-data.yaml +++ b/talos/workers/mount-longhorn-data.yaml @@ -1,3 +1,4 @@ +--- # Longhorn on Talos requires kubelet extra mounts so the CSI driver can # access the host data directory. Longhorn stores replicas under # /var/lib/longhorn on the root filesystem. diff --git a/talos/workers/reserve-ephemeral-space.yaml b/talos/workers/reserve-ephemeral-space.yaml index 9a3395acb8..8be0e88bb2 100644 --- a/talos/workers/reserve-ephemeral-space.yaml +++ b/talos/workers/reserve-ephemeral-space.yaml @@ -1,3 +1,4 @@ +--- # Existing partitions are not shrunk by configuration. The reviewed migration # first evacuates replicas and drains the worker, then resets only EPHEMERAL. apiVersion: v1alpha1 diff --git a/talos/workers/reserve-longhorn-data.yaml b/talos/workers/reserve-longhorn-data.yaml index 3badee375b..61560441cb 100644 --- a/talos/workers/reserve-longhorn-data.yaml +++ b/talos/workers/reserve-longhorn-data.yaml @@ -1,3 +1,4 @@ +--- # Reserve local block storage without claiming any CSI-attached disk (#5582). # Existing EPHEMERAL partitions are not shrunk by configuration. The protected # node-preparation workflow first evacuates all replicas and drains the worker, diff --git a/tests/audit-privileged-rbac/kyverno-test.yaml b/tests/audit-privileged-rbac/kyverno-test.yaml index eca734d77d..40712a170f 100644 --- a/tests/audit-privileged-rbac/kyverno-test.yaml +++ b/tests/audit-privileged-rbac/kyverno-test.yaml @@ -8,6 +8,11 @@ policies: resources: - resources.yaml results: + # Not listed here: the Role exclusion velero/velero-server (a current pass) and the + # two same-name lookalikes, the Role sample/velero-server and the ClusterRole + # velero-server (both fail). kyverno test pairs a row by name alone, ignoring + # namespace and kind, so it cannot tell those three apart; + # scripts/tests/test-audit-privileged-rbac.sh asserts all three by full identity. - policy: audit-privileged-rbac kind: ClusterRole result: pass @@ -65,8 +70,3 @@ results: - sample/namespaced-wildcard - sample/impersonate-serviceaccounts - sample/namespaced-clusterrole-bind - # Not listed here: the Role exclusion velero/velero-server (a current pass) and the - # two same-name lookalikes, the Role sample/velero-server and the ClusterRole - # velero-server (both fail). kyverno test pairs a row by name alone, ignoring - # namespace and kind, so it cannot tell those three apart; - # scripts/tests/test-audit-privileged-rbac.sh asserts all three by full identity. diff --git a/tests/audit-privileged-rbac/resources.yaml b/tests/audit-privileged-rbac/resources.yaml index 079af10148..5534db2d61 100644 --- a/tests/audit-privileged-rbac/resources.yaml +++ b/tests/audit-privileged-rbac/resources.yaml @@ -5,8 +5,8 @@ metadata: name: read-only-rbac rules: - apiGroups: ["rbac.authorization.k8s.io"] - resources: ["roles","clusterroles"] - verbs: ["get","list","watch"] + resources: ["roles", "clusterroles"] + verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -24,7 +24,7 @@ metadata: rules: - apiGroups: ["*"] resources: ["*"] - verbs: ["get","list","watch"] + verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -50,9 +50,9 @@ kind: ClusterRole metadata: name: wildcard-with-extra-values rules: - - apiGroups: ["apps","*"] - resources: ["pods","*"] - verbs: ["get","*"] + - apiGroups: ["apps", "*"] + resources: ["pods", "*"] + verbs: ["get", "*"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -242,7 +242,7 @@ metadata: name: read-auth-metadata rules: - apiGroups: ["authentication.k8s.io"] - resources: ["uids","userextras/scopes"] + resources: ["uids", "userextras/scopes"] verbs: ["get"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/tests/csi-sidecar-baseline-context/targets.yaml b/tests/csi-sidecar-baseline-context/targets.yaml index 2faf9c2f02..62f9e21bde 100644 --- a/tests/csi-sidecar-baseline-context/targets.yaml +++ b/tests/csi-sidecar-baseline-context/targets.yaml @@ -1,3 +1,4 @@ +--- # The live shape measured against prod 2026-09-20T21:20Z: no pod-level # securityContext, one container, no container securityContext. apiVersion: apps/v1 diff --git a/tests/csi-sidecar-baseline-context/trigger.yaml b/tests/csi-sidecar-baseline-context/trigger.yaml index dade0f4e6f..bc551ca3e2 100644 --- a/tests/csi-sidecar-baseline-context/trigger.yaml +++ b/tests/csi-sidecar-baseline-context/trigger.yaml @@ -1,3 +1,4 @@ +--- # The rule's trigger. It is one of the four matched Deployments; the policy # lists its four targets statically, so any one of them firing patches all # four. Kyverno's CLI fake client rejects the same object appearing as both a diff --git a/tests/disallow-latest-tag/kyverno-test.yaml b/tests/disallow-latest-tag/kyverno-test.yaml index 93dcfcc335..a96960ffbc 100644 --- a/tests/disallow-latest-tag/kyverno-test.yaml +++ b/tests/disallow-latest-tag/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the policy's behaviour across its ValidatingPolicy rewrite (#4996). # A versioned tag, a tag+digest and a digest alone pass, with or without a # registry port; a registry port is not a tag; ':latest' fails, as does a tagless diff --git a/tests/disallow-latest-tag/resources.yaml b/tests/disallow-latest-tag/resources.yaml index f352c4d017..7fe773604d 100644 --- a/tests/disallow-latest-tag/resources.yaml +++ b/tests/disallow-latest-tag/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Pod metadata: diff --git a/tests/enforce-flux-best-practices/kyverno-test.yaml b/tests/enforce-flux-best-practices/kyverno-test.yaml index d56b3afb0c..3711fb357c 100644 --- a/tests/enforce-flux-best-practices/kyverno-test.yaml +++ b/tests/enforce-flux-best-practices/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the policy's behaviour across its ValidatingPolicy rewrite (#5128). # A Kustomization needs a non-empty interval, timeout and retry interval, # prune: true, wait: true and a named source; a HelmRelease needs an interval, diff --git a/tests/enforce-flux-best-practices/resources.yaml b/tests/enforce-flux-best-practices/resources.yaml index e5998e3670..8bf6083fcf 100644 --- a/tests/enforce-flux-best-practices/resources.yaml +++ b/tests/enforce-flux-best-practices/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: diff --git a/tests/flux-bootstrap-health-gating/kyverno-test.yaml b/tests/flux-bootstrap-health-gating/kyverno-test.yaml index d16a5e303b..8c1541891b 100644 --- a/tests/flux-bootstrap-health-gating/kyverno-test.yaml +++ b/tests/flux-bootstrap-health-gating/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/flux-bootstrap-health-gating/missing-reliability/kyverno-test.yaml b/tests/flux-bootstrap-health-gating/missing-reliability/kyverno-test.yaml index 0a4827eb6b..df63dfb4a1 100644 --- a/tests/flux-bootstrap-health-gating/missing-reliability/kyverno-test.yaml +++ b/tests/flux-bootstrap-health-gating/missing-reliability/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/flux-bootstrap-health-gating/missing-reliability/resources.yaml b/tests/flux-bootstrap-health-gating/missing-reliability/resources.yaml index f35890debc..132b7f1c33 100644 --- a/tests/flux-bootstrap-health-gating/missing-reliability/resources.yaml +++ b/tests/flux-bootstrap-health-gating/missing-reliability/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: diff --git a/tests/flux-bootstrap-health-gating/resources.yaml b/tests/flux-bootstrap-health-gating/resources.yaml index 91644a572d..6395173b0e 100644 --- a/tests/flux-bootstrap-health-gating/resources.yaml +++ b/tests/flux-bootstrap-health-gating/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: diff --git a/tests/helm-release-enable-tests/kyverno-test.yaml b/tests/helm-release-enable-tests/kyverno-test.yaml index 4bdc429c5b..e0835a5d9a 100644 --- a/tests/helm-release-enable-tests/kyverno-test.yaml +++ b/tests/helm-release-enable-tests/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Opted-in Helm releases receive these defaults without changing other settings (#4997). apiVersion: cli.kyverno.io/v1alpha1 kind: Test diff --git a/tests/helm-release-enable-tests/label-removed/kyverno-test.yaml b/tests/helm-release-enable-tests/label-removed/kyverno-test.yaml index df644a7806..d697273b73 100644 --- a/tests/helm-release-enable-tests/label-removed/kyverno-test.yaml +++ b/tests/helm-release-enable-tests/label-removed/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/helm-release-enable-tests/label-removed/values.yaml b/tests/helm-release-enable-tests/label-removed/values.yaml index 196ba5514c..f2da4d7149 100644 --- a/tests/helm-release-enable-tests/label-removed/values.yaml +++ b/tests/helm-release-enable-tests/label-removed/values.yaml @@ -1,3 +1,4 @@ +--- # Model an UPDATE whose previous object opted in but whose incoming label was removed or disabled. apiVersion: cli.kyverno.io/v1alpha1 kind: Values diff --git a/tests/helm-release-enable-tests/patched-resources.yaml b/tests/helm-release-enable-tests/patched-resources.yaml index 1cb34755ec..3755566496 100644 --- a/tests/helm-release-enable-tests/patched-resources.yaml +++ b/tests/helm-release-enable-tests/patched-resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: diff --git a/tests/helm-release-install-crds/kyverno-test.yaml b/tests/helm-release-install-crds/kyverno-test.yaml index 01c6a512b5..f92d4ec5e0 100644 --- a/tests/helm-release-install-crds/kyverno-test.yaml +++ b/tests/helm-release-install-crds/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the policy's behaviour across its MutatingPolicy rewrite (#4997). A # HelmRelease labelled `helm.toolkit.fluxcd.io/crds: enabled` gets CreateReplace # for CRDs on install and upgrade, overriding any value it set itself and diff --git a/tests/helm-release-install-crds/patched-resources.yaml b/tests/helm-release-install-crds/patched-resources.yaml index 466c3f147c..7f94ca1779 100644 --- a/tests/helm-release-install-crds/patched-resources.yaml +++ b/tests/helm-release-install-crds/patched-resources.yaml @@ -45,4 +45,3 @@ spec: crds: CreateReplace --- - diff --git a/tests/helm-release-remediation-retries/kyverno-test.yaml b/tests/helm-release-remediation-retries/kyverno-test.yaml index 26f97712a5..ccecec486e 100644 --- a/tests/helm-release-remediation-retries/kyverno-test.yaml +++ b/tests/helm-release-remediation-retries/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Opted-in Helm releases receive these defaults without changing other settings (#4997). apiVersion: cli.kyverno.io/v1alpha1 kind: Test diff --git a/tests/helm-release-remediation-retries/label-removed/kyverno-test.yaml b/tests/helm-release-remediation-retries/label-removed/kyverno-test.yaml index 448df98f8a..c176050ae6 100644 --- a/tests/helm-release-remediation-retries/label-removed/kyverno-test.yaml +++ b/tests/helm-release-remediation-retries/label-removed/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/helm-release-remediation-retries/label-removed/values.yaml b/tests/helm-release-remediation-retries/label-removed/values.yaml index 234d2965db..351ee63ef8 100644 --- a/tests/helm-release-remediation-retries/label-removed/values.yaml +++ b/tests/helm-release-remediation-retries/label-removed/values.yaml @@ -1,3 +1,4 @@ +--- # Model an UPDATE whose previous object opted in but whose incoming label was removed or disabled. apiVersion: cli.kyverno.io/v1alpha1 kind: Values diff --git a/tests/helm-release-remediation-retries/patched-resources.yaml b/tests/helm-release-remediation-retries/patched-resources.yaml index a89ebb577e..fa21ddb5e3 100644 --- a/tests/helm-release-remediation-retries/patched-resources.yaml +++ b/tests/helm-release-remediation-retries/patched-resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: diff --git a/tests/kyverno-stale-report-prune/fixtures.yaml b/tests/kyverno-stale-report-prune/fixtures.yaml index 11c1e656a6..7d3b050238 100644 --- a/tests/kyverno-stale-report-prune/fixtures.yaml +++ b/tests/kyverno-stale-report-prune/fixtures.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: diff --git a/tests/kyverno-stale-report-prune/unevaluated-namespace-fixtures.yaml b/tests/kyverno-stale-report-prune/unevaluated-namespace-fixtures.yaml index bf60b6cbc8..874e981f86 100644 --- a/tests/kyverno-stale-report-prune/unevaluated-namespace-fixtures.yaml +++ b/tests/kyverno-stale-report-prune/unevaluated-namespace-fixtures.yaml @@ -1,3 +1,4 @@ +--- # #3155, #4234: a policy that matches a ConfigMap in each system namespace, and one # such ConfigMap per namespace. kube-system, kube-public and kube-node-lease are # dropped from the resource filters, so each must get a report. kyverno stays in diff --git a/tests/kyverno-stale-report-prune/values.yaml b/tests/kyverno-stale-report-prune/values.yaml index cc475f54c8..1e3138c590 100644 --- a/tests/kyverno-stale-report-prune/values.yaml +++ b/tests/kyverno-stale-report-prune/values.yaml @@ -1,3 +1,4 @@ +--- # Production's report settings (k8s/bases/infrastructure/controllers/kyverno/helm-release.yaml) # with a one-minute scan so the proof finishes in minutes. features: diff --git a/tests/label-baseline-workers/patched-resources.yaml b/tests/label-baseline-workers/patched-resources.yaml index 242db99846..c403edcdec 100644 --- a/tests/label-baseline-workers/patched-resources.yaml +++ b/tests/label-baseline-workers/patched-resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Node metadata: diff --git a/tests/label-baseline-workers/resources.yaml b/tests/label-baseline-workers/resources.yaml index e1013fd8be..d74ad0ddd9 100644 --- a/tests/label-baseline-workers/resources.yaml +++ b/tests/label-baseline-workers/resources.yaml @@ -1,3 +1,4 @@ +--- # A baseline worker carrying the labels the shared Talos worker config stamps. apiVersion: v1 kind: Node diff --git a/tests/longhorn-csi-plugin-baseline-context/already-clean.yaml b/tests/longhorn-csi-plugin-baseline-context/already-clean.yaml index c5f56ae941..b2c7326964 100644 --- a/tests/longhorn-csi-plugin-baseline-context/already-clean.yaml +++ b/tests/longhorn-csi-plugin-baseline-context/already-clean.yaml @@ -1,3 +1,4 @@ +--- # CONVERGENCE: the target after the retrofit has applied once. Every # precondition must be false, so a second pass produces no patch at all. apiVersion: apps/v1 diff --git a/tests/longhorn-csi-plugin-baseline-context/near-misses.yaml b/tests/longhorn-csi-plugin-baseline-context/near-misses.yaml index c9f93f7d93..0713883642 100644 --- a/tests/longhorn-csi-plugin-baseline-context/near-misses.yaml +++ b/tests/longhorn-csi-plugin-baseline-context/near-misses.yaml @@ -1,3 +1,4 @@ +--- # NEAR MISS: the same DaemonSet name in another namespace. apiVersion: apps/v1 kind: DaemonSet diff --git a/tests/longhorn-csi-plugin-baseline-context/resources.yaml b/tests/longhorn-csi-plugin-baseline-context/resources.yaml index 3c6ed87d5b..ad54b4d63a 100644 --- a/tests/longhorn-csi-plugin-baseline-context/resources.yaml +++ b/tests/longhorn-csi-plugin-baseline-context/resources.yaml @@ -1,3 +1,4 @@ +--- # The live shape measured against prod 2026-09-21: an EMPTY pod securityContext, # two privileged containers that carry a securityContext and one container that # carries none. Every existing field must survive the retrofit. diff --git a/tests/longhorn-csi-plugin-baseline-context/trigger.yaml b/tests/longhorn-csi-plugin-baseline-context/trigger.yaml index 3a6b8b0b81..77fb7e60fe 100644 --- a/tests/longhorn-csi-plugin-baseline-context/trigger.yaml +++ b/tests/longhorn-csi-plugin-baseline-context/trigger.yaml @@ -1,3 +1,4 @@ +--- # The rule's trigger. The policy has exactly one target, and Kyverno's CLI fake # client rejects the same object appearing as both a trigger and a target, so # the regression test evaluates a copy of the policy whose match NAME alone is diff --git a/tests/longhorn-engine-image-baseline-context/already-clean.yaml b/tests/longhorn-engine-image-baseline-context/already-clean.yaml index 7f8cdff61f..ced7942cd2 100644 --- a/tests/longhorn-engine-image-baseline-context/already-clean.yaml +++ b/tests/longhorn-engine-image-baseline-context/already-clean.yaml @@ -1,3 +1,4 @@ +--- # CONVERGENCE: the target after the retrofit has applied once. Every # precondition must be false, so a second pass produces no patch at all. apiVersion: apps/v1 diff --git a/tests/longhorn-engine-image-baseline-context/near-misses.yaml b/tests/longhorn-engine-image-baseline-context/near-misses.yaml index 21b91106cd..70a9e1a5fa 100644 --- a/tests/longhorn-engine-image-baseline-context/near-misses.yaml +++ b/tests/longhorn-engine-image-baseline-context/near-misses.yaml @@ -1,3 +1,4 @@ +--- # NEAR MISS: the same DaemonSet name in another namespace. apiVersion: apps/v1 kind: DaemonSet diff --git a/tests/longhorn-engine-image-baseline-context/resources.yaml b/tests/longhorn-engine-image-baseline-context/resources.yaml index 205e95da7f..8f0522e8ad 100644 --- a/tests/longhorn-engine-image-baseline-context/resources.yaml +++ b/tests/longhorn-engine-image-baseline-context/resources.yaml @@ -1,3 +1,4 @@ +--- # The live shape measured against prod 2026-09-21: an EMPTY pod securityContext # and one privileged container. Every existing field must survive the retrofit. apiVersion: apps/v1 diff --git a/tests/longhorn-engine-image-baseline-context/trigger.yaml b/tests/longhorn-engine-image-baseline-context/trigger.yaml index 3a6b8b0b81..77fb7e60fe 100644 --- a/tests/longhorn-engine-image-baseline-context/trigger.yaml +++ b/tests/longhorn-engine-image-baseline-context/trigger.yaml @@ -1,3 +1,4 @@ +--- # The rule's trigger. The policy has exactly one target, and Kyverno's CLI fake # client rejects the same object appearing as both a trigger and a target, so # the regression test evaluates a copy of the policy whose match NAME alone is diff --git a/tests/longhorn-migration-replica-exclusion/kyverno-test.yaml b/tests/longhorn-migration-replica-exclusion/kyverno-test.yaml index 1d857881be..a9ae98ec6c 100644 --- a/tests/longhorn-migration-replica-exclusion/kyverno-test.yaml +++ b/tests/longhorn-migration-replica-exclusion/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/longhorn-registrar-prestop/resources.yaml b/tests/longhorn-registrar-prestop/resources.yaml index c194565a85..b2ded0c81f 100644 --- a/tests/longhorn-registrar-prestop/resources.yaml +++ b/tests/longhorn-registrar-prestop/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apps/v1 kind: DaemonSet metadata: diff --git a/tests/prioritise-database-pods/kyverno-test.yaml b/tests/prioritise-database-pods/kyverno-test.yaml index 3f1ad95710..4e3d7b5ed2 100644 --- a/tests/prioritise-database-pods/kyverno-test.yaml +++ b/tests/prioritise-database-pods/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/prioritise-database-pods/patched-resources.yaml b/tests/prioritise-database-pods/patched-resources.yaml index f3fc490363..a9ff5842b7 100644 --- a/tests/prioritise-database-pods/patched-resources.yaml +++ b/tests/prioritise-database-pods/patched-resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Pod metadata: diff --git a/tests/prioritise-database-pods/resources.yaml b/tests/prioritise-database-pods/resources.yaml index 876989ede6..602f5e16cd 100644 --- a/tests/prioritise-database-pods/resources.yaml +++ b/tests/prioritise-database-pods/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Pod metadata: diff --git a/tests/prioritise-database-pods/update/kyverno-test.yaml b/tests/prioritise-database-pods/update/kyverno-test.yaml index 990ef2645b..93223abc31 100644 --- a/tests/prioritise-database-pods/update/kyverno-test.yaml +++ b/tests/prioritise-database-pods/update/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/prioritise-database-pods/update/values.yaml b/tests/prioritise-database-pods/update/values.yaml index 37a7019ef4..dab25f2928 100644 --- a/tests/prioritise-database-pods/update/values.yaml +++ b/tests/prioritise-database-pods/update/values.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Values metadata: diff --git a/tests/prioritise-database-pods/values.yaml b/tests/prioritise-database-pods/values.yaml index 78287214b2..82b86be385 100644 --- a/tests/prioritise-database-pods/values.yaml +++ b/tests/prioritise-database-pods/values.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Values metadata: diff --git a/tests/require-baseline-workers-for-hcloud-volumes/context.yaml b/tests/require-baseline-workers-for-hcloud-volumes/context.yaml index 4d5061b531..37001e6f1c 100644 --- a/tests/require-baseline-workers-for-hcloud-volumes/context.yaml +++ b/tests/require-baseline-workers-for-hcloud-volumes/context.yaml @@ -1,3 +1,4 @@ +--- # The Kyverno CLI has no API server, so the claims the policies list are # supplied here. Every claim a Pod in resources.yaml mounts uses the hcloud # class; the sibling longhorn-claim/ fixture proves a non-hcloud claim is not diff --git a/tests/restrict-arc-openbao-certificate/kyverno-test.yaml b/tests/restrict-arc-openbao-certificate/kyverno-test.yaml index 555bffd49a..ae2f0295ff 100644 --- a/tests/restrict-arc-openbao-certificate/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-certificate/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/restrict-arc-openbao-certificate/resources.yaml b/tests/restrict-arc-openbao-certificate/resources.yaml index c76ef88b36..3c570866a0 100644 --- a/tests/restrict-arc-openbao-certificate/resources.yaml +++ b/tests/restrict-arc-openbao-certificate/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cert-manager.io/v1 kind: Certificate metadata: diff --git a/tests/restrict-arc-openbao-listener-binary-data-delete/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-binary-data-delete/kyverno-test.yaml index d90c3c1dab..95235b0cdc 100644 --- a/tests/restrict-arc-openbao-listener-binary-data-delete/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-binary-data-delete/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the ClusterPolicy's behaviour before its ValidatingPolicy rewrite (#4995): # binaryData beside the audited listener is denied. apiVersion: cli.kyverno.io/v1alpha1 diff --git a/tests/restrict-arc-openbao-listener-binary-data-update/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-binary-data-update/kyverno-test.yaml index 0437774590..9ef69aa264 100644 --- a/tests/restrict-arc-openbao-listener-binary-data-update/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-binary-data-update/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # UPDATE fixtures supply the same old/new object. Preserve the legacy # grandfathering skip for an existing violation; distinct transitions require # the native admission driver because the CLI deliberately clones the object. diff --git a/tests/restrict-arc-openbao-listener-binary-data/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-binary-data/kyverno-test.yaml index 9afb5f46d6..f55daac258 100644 --- a/tests/restrict-arc-openbao-listener-binary-data/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-binary-data/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the ClusterPolicy's behaviour before its ValidatingPolicy rewrite (#4995): # binaryData beside the audited listener is denied. apiVersion: cli.kyverno.io/v1alpha1 diff --git a/tests/restrict-arc-openbao-listener-binary-data/resources.yaml b/tests/restrict-arc-openbao-listener-binary-data/resources.yaml index 4c74641a40..6eab3810da 100644 --- a/tests/restrict-arc-openbao-listener-binary-data/resources.yaml +++ b/tests/restrict-arc-openbao-listener-binary-data/resources.yaml @@ -1,3 +1,4 @@ +--- # The listener in openbao with binaryData beside the audited listener. apiVersion: v1 kind: ConfigMap diff --git a/tests/restrict-arc-openbao-listener-changed-listener-delete/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-changed-listener-delete/kyverno-test.yaml index 3e70d2c16a..d02a15721e 100644 --- a/tests/restrict-arc-openbao-listener-changed-listener-delete/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-changed-listener-delete/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the ClusterPolicy's behaviour before its ValidatingPolicy rewrite (#4995): # the listener with client certificates no longer disabled is denied. apiVersion: cli.kyverno.io/v1alpha1 diff --git a/tests/restrict-arc-openbao-listener-changed-listener-update/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-changed-listener-update/kyverno-test.yaml index e9b54309f7..66df3e2e81 100644 --- a/tests/restrict-arc-openbao-listener-changed-listener-update/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-changed-listener-update/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # UPDATE fixtures supply the same old/new object. Preserve the legacy # grandfathering skip for an existing violation; distinct transitions require # the native admission driver because the CLI deliberately clones the object. diff --git a/tests/restrict-arc-openbao-listener-changed-listener/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-changed-listener/kyverno-test.yaml index cbc08ae86c..1369525d38 100644 --- a/tests/restrict-arc-openbao-listener-changed-listener/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-changed-listener/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the ClusterPolicy's behaviour before its ValidatingPolicy rewrite (#4995): # the listener with client certificates no longer disabled is denied. apiVersion: cli.kyverno.io/v1alpha1 diff --git a/tests/restrict-arc-openbao-listener-changed-listener/resources.yaml b/tests/restrict-arc-openbao-listener-changed-listener/resources.yaml index 1217cde42d..12a1c6913d 100644 --- a/tests/restrict-arc-openbao-listener-changed-listener/resources.yaml +++ b/tests/restrict-arc-openbao-listener-changed-listener/resources.yaml @@ -1,3 +1,4 @@ +--- # The listener in openbao with the listener with client certificates no longer disabled. apiVersion: v1 kind: ConfigMap diff --git a/tests/restrict-arc-openbao-listener-delete/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-delete/kyverno-test.yaml index 6a0dad9884..a2f00be3a9 100644 --- a/tests/restrict-arc-openbao-listener-delete/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-delete/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the listener guard against independent literal resource outcomes. # The audited listener in openbao passes; the same name anywhere else fails; a # ConfigMap with another name is outside the rule. Changed content in openbao is diff --git a/tests/restrict-arc-openbao-listener-extra-data-delete/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-extra-data-delete/kyverno-test.yaml index e778275614..ceb7b6e778 100644 --- a/tests/restrict-arc-openbao-listener-extra-data-delete/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-extra-data-delete/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the ClusterPolicy's behaviour before its ValidatingPolicy rewrite (#4995): # a second data key beside listener.hcl is denied. apiVersion: cli.kyverno.io/v1alpha1 diff --git a/tests/restrict-arc-openbao-listener-extra-data-update/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-extra-data-update/kyverno-test.yaml index 8e99b40325..c33a12552e 100644 --- a/tests/restrict-arc-openbao-listener-extra-data-update/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-extra-data-update/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # UPDATE fixtures supply the same old/new object. Preserve the legacy # grandfathering skip for an existing violation; distinct transitions require # the native admission driver because the CLI deliberately clones the object. diff --git a/tests/restrict-arc-openbao-listener-extra-data/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-extra-data/kyverno-test.yaml index 67046d9bb6..d59264324f 100644 --- a/tests/restrict-arc-openbao-listener-extra-data/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-extra-data/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the ClusterPolicy's behaviour before its ValidatingPolicy rewrite (#4995): # a second data key beside listener.hcl is denied. apiVersion: cli.kyverno.io/v1alpha1 diff --git a/tests/restrict-arc-openbao-listener-extra-data/resources.yaml b/tests/restrict-arc-openbao-listener-extra-data/resources.yaml index cb43bfafda..1027c41882 100644 --- a/tests/restrict-arc-openbao-listener-extra-data/resources.yaml +++ b/tests/restrict-arc-openbao-listener-extra-data/resources.yaml @@ -1,3 +1,4 @@ +--- # The listener in openbao with a second data key beside listener.hcl. apiVersion: v1 kind: ConfigMap diff --git a/tests/restrict-arc-openbao-listener-update/kyverno-test.yaml b/tests/restrict-arc-openbao-listener-update/kyverno-test.yaml index 6218c1bf52..19a792e010 100644 --- a/tests/restrict-arc-openbao-listener-update/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener-update/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # UPDATE fixtures supply the same old/new object. Preserve the legacy # grandfathering skip for an existing violation; distinct transitions require # the native admission driver because the CLI deliberately clones the object. diff --git a/tests/restrict-arc-openbao-listener/kyverno-test.yaml b/tests/restrict-arc-openbao-listener/kyverno-test.yaml index ee304a2448..11fe0491dd 100644 --- a/tests/restrict-arc-openbao-listener/kyverno-test.yaml +++ b/tests/restrict-arc-openbao-listener/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the listener guard against independent literal resource outcomes. # The audited listener in openbao passes; the same name anywhere else fails; a # ConfigMap with another name is outside the rule. Changed content in openbao is diff --git a/tests/restrict-arc-openbao-listener/resources.yaml b/tests/restrict-arc-openbao-listener/resources.yaml index c44b1f9fbb..2d573b5108 100644 --- a/tests/restrict-arc-openbao-listener/resources.yaml +++ b/tests/restrict-arc-openbao-listener/resources.yaml @@ -1,3 +1,4 @@ +--- # The audited listener, byte for byte as # k8s/providers/hetzner/infrastructure/controllers/openbao/transport/config-map-listener.yaml # ships it. diff --git a/tests/restrict-arc-retirement-credential-sync-rewritten/kyverno-test.yaml b/tests/restrict-arc-retirement-credential-sync-rewritten/kyverno-test.yaml index f513bd6837..1566710843 100644 --- a/tests/restrict-arc-retirement-credential-sync-rewritten/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-credential-sync-rewritten/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the fence-credential-recreation rule before the ValidatingPolicy rewrite # (#4995): past the baseline phase (here "drained"), an update that rewrites its spec is denied on # the ARC credential-sync ExternalSecret. values.yaml supplies the journal the diff --git a/tests/restrict-arc-retirement-credential-sync-rewritten/resources.yaml b/tests/restrict-arc-retirement-credential-sync-rewritten/resources.yaml index 027ae4f692..07cb70f882 100644 --- a/tests/restrict-arc-retirement-credential-sync-rewritten/resources.yaml +++ b/tests/restrict-arc-retirement-credential-sync-rewritten/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: diff --git a/tests/restrict-arc-retirement-credential-sync-unchanged/kyverno-test.yaml b/tests/restrict-arc-retirement-credential-sync-unchanged/kyverno-test.yaml index f87c9be10b..a4a589be63 100644 --- a/tests/restrict-arc-retirement-credential-sync-unchanged/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-credential-sync-unchanged/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the fence-credential-recreation rule before the ValidatingPolicy rewrite # (#4995): past the baseline phase (here "drained"), an update that leaves its spec unchanged is allowed on # the ARC credential-sync ExternalSecret. values.yaml supplies the journal the diff --git a/tests/restrict-arc-retirement-credential-sync-unchanged/resources.yaml b/tests/restrict-arc-retirement-credential-sync-unchanged/resources.yaml index 027ae4f692..07cb70f882 100644 --- a/tests/restrict-arc-retirement-credential-sync-unchanged/resources.yaml +++ b/tests/restrict-arc-retirement-credential-sync-unchanged/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: diff --git a/tests/restrict-arc-retirement-journal-admin-rewrite/kyverno-test.yaml b/tests/restrict-arc-retirement-journal-admin-rewrite/kyverno-test.yaml index 134999ec9f..86f488ad94 100644 --- a/tests/restrict-arc-retirement-journal-admin-rewrite/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-journal-admin-rewrite/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the preserve-retirement-journal rule before the ValidatingPolicy rewrite # (#4995): protected administrator recovery (system:masters) may replace the journal. # values.yaml supplies the previous Namespace and user-info.yaml the requester; diff --git a/tests/restrict-arc-retirement-journal-admin-rewrite/resources.yaml b/tests/restrict-arc-retirement-journal-admin-rewrite/resources.yaml index c9164b9ec6..6b46dcfae8 100644 --- a/tests/restrict-arc-retirement-journal-admin-rewrite/resources.yaml +++ b/tests/restrict-arc-retirement-journal-admin-rewrite/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: diff --git a/tests/restrict-arc-retirement-journal-flux-rewrite/kyverno-test.yaml b/tests/restrict-arc-retirement-journal-flux-rewrite/kyverno-test.yaml index f7f79137cb..e62ccfaf4e 100644 --- a/tests/restrict-arc-retirement-journal-flux-rewrite/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-journal-flux-rewrite/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the preserve-retirement-journal rule before the ValidatingPolicy rewrite # (#4995): a controller without system:masters may not replace the retirement journal. # values.yaml supplies the previous Namespace and user-info.yaml the requester; diff --git a/tests/restrict-arc-retirement-journal-flux-rewrite/resources.yaml b/tests/restrict-arc-retirement-journal-flux-rewrite/resources.yaml index c9164b9ec6..6b46dcfae8 100644 --- a/tests/restrict-arc-retirement-journal-flux-rewrite/resources.yaml +++ b/tests/restrict-arc-retirement-journal-flux-rewrite/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: diff --git a/tests/restrict-arc-retirement-journal-flux-unchanged/kyverno-test.yaml b/tests/restrict-arc-retirement-journal-flux-unchanged/kyverno-test.yaml index b3bde63754..bbc4fd7e37 100644 --- a/tests/restrict-arc-retirement-journal-flux-unchanged/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-journal-flux-unchanged/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the preserve-retirement-journal rule before the ValidatingPolicy rewrite # (#4995): a controller without system:masters may update the Namespace when the journal is unchanged. # values.yaml supplies the previous Namespace and user-info.yaml the requester; diff --git a/tests/restrict-arc-retirement-journal-flux-unchanged/resources.yaml b/tests/restrict-arc-retirement-journal-flux-unchanged/resources.yaml index 61d6139605..f0dbd8cae9 100644 --- a/tests/restrict-arc-retirement-journal-flux-unchanged/resources.yaml +++ b/tests/restrict-arc-retirement-journal-flux-unchanged/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: diff --git a/tests/restrict-arc-retirement-listener-pod/kyverno-test.yaml b/tests/restrict-arc-retirement-listener-pod/kyverno-test.yaml index a3dd983b63..a9ef290cc0 100644 --- a/tests/restrict-arc-retirement-listener-pod/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-listener-pod/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the fence-retired-listener-pod rule before the ValidatingPolicy rewrite # (#4995). The retirement journal's phase is read from the arc-runners # Namespace through an API call, which kyverno test cannot make, so values.yaml diff --git a/tests/restrict-arc-retirement-namespace-delete-unannotated/kyverno-test.yaml b/tests/restrict-arc-retirement-namespace-delete-unannotated/kyverno-test.yaml index 36ac435670..79bdf91d3e 100644 --- a/tests/restrict-arc-retirement-namespace-delete-unannotated/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-namespace-delete-unannotated/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Allowed control for the preserve-retiring-namespace fixture beside it (#4995): # with no retirement journal annotation, deleting arc-runners is allowed. It # needs its own directory because both cases are the one arc-runners Namespace. diff --git a/tests/restrict-arc-retirement-namespace-delete-unannotated/resources.yaml b/tests/restrict-arc-retirement-namespace-delete-unannotated/resources.yaml index 4e52c97e36..a3559ddd44 100644 --- a/tests/restrict-arc-retirement-namespace-delete-unannotated/resources.yaml +++ b/tests/restrict-arc-retirement-namespace-delete-unannotated/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: diff --git a/tests/restrict-arc-retirement-namespace-delete/kyverno-test.yaml b/tests/restrict-arc-retirement-namespace-delete/kyverno-test.yaml index 3025ae33e2..041c0df5ba 100644 --- a/tests/restrict-arc-retirement-namespace-delete/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-namespace-delete/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the preserve-retiring-namespace rule before the ValidatingPolicy rewrite # (#4995): deleting arc-runners is denied while it carries the retirement # journal annotation, and allowed when it does not. kyverno test presents the diff --git a/tests/restrict-arc-retirement-namespace-delete/resources.yaml b/tests/restrict-arc-retirement-namespace-delete/resources.yaml index 4f1c12d791..a59d17cb0f 100644 --- a/tests/restrict-arc-retirement-namespace-delete/resources.yaml +++ b/tests/restrict-arc-retirement-namespace-delete/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Namespace metadata: diff --git a/tests/restrict-arc-retirement-secret-absent/kyverno-test.yaml b/tests/restrict-arc-retirement-secret-absent/kyverno-test.yaml index 16fa953a1d..158fb6e274 100644 --- a/tests/restrict-arc-retirement-secret-absent/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-secret-absent/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the fence-retired-secret rule before the ValidatingPolicy rewrite # (#4995): with the retirement journal in phase "absent", creating the ARC # GitHub App credential is denied. values.yaml supplies the journal the policy diff --git a/tests/restrict-arc-retirement-secret-absent/resources.yaml b/tests/restrict-arc-retirement-secret-absent/resources.yaml index f0bd143af9..97a7205904 100644 --- a/tests/restrict-arc-retirement-secret-absent/resources.yaml +++ b/tests/restrict-arc-retirement-secret-absent/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Secret metadata: diff --git a/tests/restrict-arc-retirement-secret-restored/kyverno-test.yaml b/tests/restrict-arc-retirement-secret-restored/kyverno-test.yaml index 2061eac2d5..9d025e3b9e 100644 --- a/tests/restrict-arc-retirement-secret-restored/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-secret-restored/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the fence-retired-secret rule before the ValidatingPolicy rewrite # (#4995): with the retirement journal in phase "restored", creating the ARC # GitHub App credential is denied. values.yaml supplies the journal the policy diff --git a/tests/restrict-arc-retirement-secret-restored/resources.yaml b/tests/restrict-arc-retirement-secret-restored/resources.yaml index f0bd143af9..97a7205904 100644 --- a/tests/restrict-arc-retirement-secret-restored/resources.yaml +++ b/tests/restrict-arc-retirement-secret-restored/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Secret metadata: diff --git a/tests/restrict-arc-retirement-secret-uninstalled/kyverno-test.yaml b/tests/restrict-arc-retirement-secret-uninstalled/kyverno-test.yaml index e6c76029ee..2288aefd79 100644 --- a/tests/restrict-arc-retirement-secret-uninstalled/kyverno-test.yaml +++ b/tests/restrict-arc-retirement-secret-uninstalled/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the fence-retired-secret rule before the ValidatingPolicy rewrite # (#4995): with the retirement journal in phase "uninstalled", creating the ARC # GitHub App credential is allowed. values.yaml supplies the journal the policy diff --git a/tests/restrict-arc-retirement-secret-uninstalled/resources.yaml b/tests/restrict-arc-retirement-secret-uninstalled/resources.yaml index f0bd143af9..97a7205904 100644 --- a/tests/restrict-arc-retirement-secret-uninstalled/resources.yaml +++ b/tests/restrict-arc-retirement-secret-uninstalled/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Secret metadata: diff --git a/tests/restrict-tenant-network-policies/node-group-resources.yaml b/tests/restrict-tenant-network-policies/node-group-resources.yaml index fc7fd4fe30..8008e51a18 100644 --- a/tests/restrict-tenant-network-policies/node-group-resources.yaml +++ b/tests/restrict-tenant-network-policies/node-group-resources.yaml @@ -1,3 +1,4 @@ +--- # Node and provider selectors are outside the tenant namespace boundary. # Empty lists and nulls are paired with a valid endpoint selector, so only # the forbidden field can explain their refusal. diff --git a/tests/restrict-tenant-webapps/kyverno-test.yaml b/tests/restrict-tenant-webapps/kyverno-test.yaml index b88d04eec0..36e2838c1b 100644 --- a/tests/restrict-tenant-webapps/kyverno-test.yaml +++ b/tests/restrict-tenant-webapps/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/restrict-tenant-webapps/resources.yaml b/tests/restrict-tenant-webapps/resources.yaml index baff039210..8039297a7a 100644 --- a/tests/restrict-tenant-webapps/resources.yaml +++ b/tests/restrict-tenant-webapps/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: kro.run/v1alpha1 kind: WebApp metadata: diff --git a/tests/restrict-tenant-webapps/user-info.yaml b/tests/restrict-tenant-webapps/user-info.yaml index e32c2e6217..d7240787dc 100644 --- a/tests/restrict-tenant-webapps/user-info.yaml +++ b/tests/restrict-tenant-webapps/user-info.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: UserInfo metadata: diff --git a/tests/restrict-webapp-child-deletion/kyverno-test.yaml b/tests/restrict-webapp-child-deletion/kyverno-test.yaml index f890351f1a..cc54b83571 100644 --- a/tests/restrict-webapp-child-deletion/kyverno-test.yaml +++ b/tests/restrict-webapp-child-deletion/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/restrict-webapp-child-deletion/ordinary-writer/kyverno-test.yaml b/tests/restrict-webapp-child-deletion/ordinary-writer/kyverno-test.yaml index 0f7f483a94..ed9ab8bade 100644 --- a/tests/restrict-webapp-child-deletion/ordinary-writer/kyverno-test.yaml +++ b/tests/restrict-webapp-child-deletion/ordinary-writer/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/restrict-webapp-child-deletion/resources.yaml b/tests/restrict-webapp-child-deletion/resources.yaml index fe542b8b6a..8263899c56 100644 --- a/tests/restrict-webapp-child-deletion/resources.yaml +++ b/tests/restrict-webapp-child-deletion/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Service metadata: diff --git a/tests/restrict-webapp-child-deletion/values.yaml b/tests/restrict-webapp-child-deletion/values.yaml index a731cb180b..e180a47686 100644 --- a/tests/restrict-webapp-child-deletion/values.yaml +++ b/tests/restrict-webapp-child-deletion/values.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Values metadata: diff --git a/tests/restrict-webapp-child-ownership/kyverno-test.yaml b/tests/restrict-webapp-child-ownership/kyverno-test.yaml index fa7961eb38..f001d6c350 100644 --- a/tests/restrict-webapp-child-ownership/kyverno-test.yaml +++ b/tests/restrict-webapp-child-ownership/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: Test metadata: diff --git a/tests/restrict-webapp-child-ownership/resources.yaml b/tests/restrict-webapp-child-ownership/resources.yaml index 623fec0919..4bb64e7f30 100644 --- a/tests/restrict-webapp-child-ownership/resources.yaml +++ b/tests/restrict-webapp-child-ownership/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: v1 kind: Service metadata: diff --git a/tests/restrict-webapp-child-ownership/user-info.yaml b/tests/restrict-webapp-child-ownership/user-info.yaml index dc9059c123..f017c364b8 100644 --- a/tests/restrict-webapp-child-ownership/user-info.yaml +++ b/tests/restrict-webapp-child-ownership/user-info.yaml @@ -1,3 +1,4 @@ +--- apiVersion: cli.kyverno.io/v1alpha1 kind: UserInfo metadata: diff --git a/tests/validate-pdb-drain-safe/kyverno-test.yaml b/tests/validate-pdb-drain-safe/kyverno-test.yaml index 188ce0b685..3f96a2a616 100644 --- a/tests/validate-pdb-drain-safe/kyverno-test.yaml +++ b/tests/validate-pdb-drain-safe/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the policy's behaviour across its ValidatingPolicy rewrite (#4996). # A maxUnavailable PDB passes and a minAvailable PDB fails. Each exclusion is # pinned: a CloudNativePG-labelled PDB, a KSail-installed PDB by name, and a diff --git a/tests/validate-pdb-drain-safe/resources.yaml b/tests/validate-pdb-drain-safe/resources.yaml index 08d3c603c1..893cb231b8 100644 --- a/tests/validate-pdb-drain-safe/resources.yaml +++ b/tests/validate-pdb-drain-safe/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: diff --git a/tests/validate-replica-ceiling/kyverno-test.yaml b/tests/validate-replica-ceiling/kyverno-test.yaml index 287eb1a5d3..0d83fa37ac 100644 --- a/tests/validate-replica-ceiling/kyverno-test.yaml +++ b/tests/validate-replica-ceiling/kyverno-test.yaml @@ -1,3 +1,4 @@ +--- # Pins the policy's behaviour across its ValidatingPolicy rewrite (#4996). # Each of the three rules passes at the ceiling of 3 and fails above it. An unset # Deployment replica count passes (it defaults to 1); an unset ScaledObject diff --git a/tests/validate-replica-ceiling/resources.yaml b/tests/validate-replica-ceiling/resources.yaml index f3d0d0fe7b..979c970bd1 100644 --- a/tests/validate-replica-ceiling/resources.yaml +++ b/tests/validate-replica-ceiling/resources.yaml @@ -1,3 +1,4 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: From 64d132f8f72c5655262eb2fb82ecbc9573f033c1 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 4 Nov 2026 01:34:10 +0100 Subject: [PATCH 12/19] fix(deps): raise vulnerable transitive Go modules grype reads from go.sum Grype reads go.sum as well as go.mod, so modules a build never selects still count. render-helm-chart now requires the fixed grpc, klauspost/compress, go-jose, distribution, edwards25519 and OpenTelemetry releases; three test modules require klauspost/compress v1.19.1. go mod tidy keeps the requirements and is idempotent, and helm still builds. Co-Authored-By: Claude Opus 5.5 --- .../testdata/retirement-fence-proof/go.mod | 1 + .../testdata/retirement-fence-proof/go.sum | 4 +- scripts/render-helm-chart/go.mod | 15 ++- scripts/render-helm-chart/go.sum | 119 +++++++++--------- .../tests/gateway-receiver-admission/go.mod | 1 + .../tests/gateway-receiver-admission/go.sum | 4 +- .../testdata/native-proof/go.mod | 1 + .../testdata/native-proof/go.sum | 4 +- 8 files changed, 79 insertions(+), 70 deletions(-) diff --git a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod index 4ef14f4d4a..ae561d9d0a 100644 --- a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod @@ -33,6 +33,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.19.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect diff --git a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum index 516a1e9e4c..bd2f6f2d91 100644 --- a/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum +++ b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.sum @@ -56,8 +56,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= diff --git a/scripts/render-helm-chart/go.mod b/scripts/render-helm-chart/go.mod index 2114bceb60..ad51c77a2c 100644 --- a/scripts/render-helm-chart/go.mod +++ b/scripts/render-helm-chart/go.mod @@ -8,6 +8,7 @@ require helm.sh/helm/v4 v4.2.4 // indirect require ( dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect @@ -24,6 +25,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/distribution/v3 v3.1.2 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -34,7 +36,8 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.6 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect @@ -50,6 +53,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.19.1 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.12.3 // indirect @@ -77,13 +81,16 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/testify v1.12.1 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect github.com/tetratelabs/wazero v1.12.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/proto/otlp v1.10.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 // indirect golang.org/x/net v0.62.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect diff --git a/scripts/render-helm-chart/go.sum b/scripts/render-helm-chart/go.sum index 3a0e745246..33c736205f 100644 --- a/scripts/render-helm-chart/go.sum +++ b/scripts/render-helm-chart/go.sum @@ -1,7 +1,8 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -30,8 +31,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4= -github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -54,8 +53,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.1.1 h1:KUbk7C8CfaLXy8kbf/hGq9cad/wCoLB6dbWH6DMbmX0= -github.com/distribution/distribution/v3 v3.1.1/go.mod h1:d7lXwZpph0bVcOj4Aqn0nMrWHIwRQGdiV5TLeI+/w6Y= +github.com/distribution/distribution/v3 v3.1.2 h1:/Bv2YIqqSR00HiO49FT93hovy6r9IlrzwUDsJjCN5qg= +github.com/distribution/distribution/v3 v3.1.2/go.mod h1:Oz0Drs2Tx3iTXzK3DhUD4otfTfqlCxlWVC7QJeXWBP0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= @@ -64,8 +63,8 @@ github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0 github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4= github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY= +github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= @@ -92,10 +91,10 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= -github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-jose/go-jose/v4 v4.1.6 h1:sNRkftxFJxwsILiQukqp5z83QBSmntVwmgRY7AqBDO4= +github.com/go-jose/go-jose/v4 v4.1.6/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -131,8 +130,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= @@ -149,8 +148,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -239,8 +238,8 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= @@ -249,14 +248,14 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= @@ -273,54 +272,55 @@ go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9a go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0 h1:WseeVYf5dJZTsyPiyW5L14k5qsSibqXAMTSiFEDiWr0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.21.0/go.mod h1:SiLZnQS6Qk2eCpvr2CH/XMAOa64TWGXxEZJZCpD2Lmc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0 h1:fvNHGyo3CdRv/DQveXqhqBxnKTDyRaC5sMSQxilX/A0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.21.0/go.mod h1:zyGrjRKL2B/6+Jc/m4/otPoZqV2MY9ZjC/aBraRO7zc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0 h1:fG5MCxGz8+2VtrN/WgqSpJFctVz24gpxj8CxkKmc8Ww= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.45.0/go.mod h1:BmAYTn+3ysbRe+IU2msxmf5Rx3g6DHvex+tWI3LdhYI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8= go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs= go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.21.0 h1:2lpf4hnrasYIsUyEXwnTZq5lsxrMm4T2Bwb06IctAZQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.21.0/go.mod h1:YWOW6h7jwApz9Pl76ie/izUsSPj0s2MdIlpqbPqaf3U= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= -go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= -go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= -go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.opentelemetry.io/otel/log v0.21.0 h1:SLsVDGmtyBrdw8/a2Z0bOIxou/+bN4z56GebH7T0LvA= +go.opentelemetry.io/otel/log v0.21.0/go.mod h1:iReetQrZL9Wyg84cCkOoCmqDHS5RCFfyxC7J+r8fn8g= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/log v0.21.0 h1:QsE7XSR0ktQdKmRKGnR+f1ObGF32WG+7MER/P9KgmYc= +go.opentelemetry.io/otel/sdk/log v0.21.0/go.mod h1:m9mApjCoD2/1QuKCAptjv+BrG9WKOvQLVdNx+iBldTo= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2 h1:zGDaB+/Mus2mScKvZ23k+W9NAUbP56sNM9SptgvWI5A= golang.org/x/crypto v0.58.1-0.20261009181718-2f1f834330b2/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= @@ -343,12 +343,13 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= -google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -358,8 +359,6 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/tests/gateway-receiver-admission/go.mod b/scripts/tests/gateway-receiver-admission/go.mod index 6850ec1f17..f507953297 100644 --- a/scripts/tests/gateway-receiver-admission/go.mod +++ b/scripts/tests/gateway-receiver-admission/go.mod @@ -33,6 +33,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.19.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect diff --git a/scripts/tests/gateway-receiver-admission/go.sum b/scripts/tests/gateway-receiver-admission/go.sum index 516a1e9e4c..bd2f6f2d91 100644 --- a/scripts/tests/gateway-receiver-admission/go.sum +++ b/scripts/tests/gateway-receiver-admission/go.sum @@ -56,8 +56,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= diff --git a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod index c2926036cd..ab724b903c 100644 --- a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod +++ b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod @@ -33,6 +33,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.19.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect diff --git a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum index 516a1e9e4c..bd2f6f2d91 100644 --- a/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum +++ b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.sum @@ -56,8 +56,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= From ec026e2943b890dbbcb311373a28f7ce98564592 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 4 Nov 2026 01:34:11 +0100 Subject: [PATCH 13/19] fix(lint): match go.sum lines whose reported text starts with a newline Betterleaks reports a finding's line from the preceding newline, so the anchored go.sum allowlist never matched the two quoted lines in the Kyverno background patch. Co-Authored-By: Claude Opus 5.5 --- .betterleaks.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.betterleaks.toml b/.betterleaks.toml index 68af4c591f..9e7ee10c0f 100644 --- a/.betterleaks.toml +++ b/.betterleaks.toml @@ -27,4 +27,6 @@ id = "generic-api-key" [[rules.allowlists]] description = "Go module checksums: an `h1:` go.sum line, as quoted in a vendored go.sum diff, is a public hash." regexTarget = "line" -regexes = ['''^[+ -]?\S+ v\S+ h1:[A-Za-z0-9+/]{43}=\s*$'''] +# The reported line can begin with the preceding newline, so allow leading whitespace before the +# optional diff marker. +regexes = ['''^\s*[+ -]?\S+ v\S+ h1:[A-Za-z0-9+/]{43}=\s*$'''] From 081cbeaec60c60a10123718f6c2aad8b094fff25 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 4 Nov 2026 01:34:11 +0100 Subject: [PATCH 14/19] test(refresh-flux-ghcr-auth): run mutants through Bash to avoid ETXTBSY A parallel fork can hold a freshly written mutant open for writing, so executing it directly failed with "text file busy" on Linux. Both mutation tests now let Bash read the file, as the shared test helper already does. Co-Authored-By: Claude Opus 5.5 --- .../lease_after_release_test.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/tests/refresh-flux-ghcr-auth/lease_after_release_test.go b/scripts/tests/refresh-flux-ghcr-auth/lease_after_release_test.go index 2df67f384f..a77263f76c 100644 --- a/scripts/tests/refresh-flux-ghcr-auth/lease_after_release_test.go +++ b/scripts/tests/refresh-flux-ghcr-auth/lease_after_release_test.go @@ -69,12 +69,11 @@ func TestAfterReleaseRegressionDetectsEachAblatedProductionFence(t *testing.T) { if err := file.Close(); err != nil { t.Fatal(err) } - if err := os.Chmod(path, 0o700); err != nil { - t.Fatal(err) - } - result := f.runHelperExecutable(validConfig(), nil, map[string]string{ + // A parallel fork can briefly retain the mutant's writer after Close, + // so executing the new file can fail with ETXTBSY. Let Bash read it. + result := f.runHelperExecutable(validConfig(), []string{path}, map[string]string{ "FAKE_SYNC_LEASE_ORPHANED_HEARTBEAT_WRITE_AFTER_RELEASE": "true", - }, false, path) + }, false, "bash") // The mutated helper completes normally. The rejection assertion, // rather than a syntax error or the fake's old mandatory-test policy, // must be what distinguishes it from the real helper. @@ -143,13 +142,12 @@ func TestDeletedLeaseDiagnosticRegressionRejectsAnAppliedMutation(t *testing.T) if err := file.Close(); err != nil { t.Fatal(err) } - if err := os.Chmod(path, 0o700); err != nil { - t.Fatal(err) - } f := newFixture(t) - result := f.runHelperExecutable(validConfig(), nil, map[string]string{ + // A parallel fork can briefly retain the mutant's writer after Close, so + // executing the new file can fail with ETXTBSY. Let Bash read it. + result := f.runHelperExecutable(validConfig(), []string{path}, map[string]string{ "FAKE_SYNC_LEASE_DELETED_BEFORE_RELEASE": "true", - }, false, path) + }, false, "bash") // The applied mutation reaches release and exhausts the invalid-state path. // It still fails overall; only the missing diagnostic distinguishes it, so // asserting a non-zero exit alone would not protect the deleted-Lease branch. From 42d9b3157dbb29562d1b0fff68ff7fac9e522894 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 4 Nov 2026 03:52:51 +0100 Subject: [PATCH 15/19] ci(lint): keep byte-exact fixtures out of prettier and markdownlint The organization Go workflow lints the whole repository with APPLY_FIXES, so a formatter there rewrites files whose bytes are the point: the frozen EKS approval history (sha256-pinned by its test), the jq-written RBAC baselines (compared with diff -u), the upstream flagd schemas (sha256 recorded in the README) and the captured Stalwart store output. Co-Authored-By: Claude Opus 5.5 --- .mega-linter.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.mega-linter.yml b/.mega-linter.yml index 9352bda2db..841b05865c 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -352,7 +352,18 @@ DISABLE_ERRORS_LINTERS: SPELL_CSPELL_FILTER_REGEX_EXCLUDE: (k8s/.*|\.agents/.*) SPELL_LYCHEE_FILTER_REGEX_EXCLUDE: (k8s/.*|\.agents/.*) -MARKDOWN_MARKDOWNLINT_FILTER_REGEX_EXCLUDE: \.agents/.* +# approved-surface-history.md is frozen: history_frozen_test.go accepts only its recorded sha256, +# so markdownlint's fixes would fail that test. New approvals go in approved-surface.txt instead. +MARKDOWN_MARKDOWNLINT_FILTER_REGEX_EXCLUDE: (\.agents/.*|scripts/validate-eks-ci-role-policy/approved-surface-history\.md) + +# prettier rewrites JSON in place, and these three sets must keep their exact bytes: +# * tests/chart-rendered-rbac-policy/ baselines are written by jq (UPDATE_BASELINE=1) and compared +# with `diff -u`, so prettier's layout fails the test that owns them. +# * scripts/validate-feature-flags/schema/ holds upstream flagd schemas whose sha256 the README +# records; reformatting them breaks that provenance. +# * scripts/diagnose-email-startup/testdata/ holds output captured from a real Stalwart store, and +# the test is meant to read what the store wrote. +JSON_PRETTIER_FILTER_REGEX_EXCLUDE: (tests/chart-rendered-rbac-policy/.*\.json|scripts/validate-feature-flags/schema/.*\.json|scripts/diagnose-email-startup/testdata/.*\.json) # The only HTML file here is a byte-exact copy of the released Hubble UI v0.13.6 entry point, which # check-hubble-ui-streaming's regression test reads and requires unaltered, minifier output included. From 3ddf5e61aea6c24e7fd6c7d14dcee3620a03317f Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Wed, 4 Nov 2026 03:52:53 +0100 Subject: [PATCH 16/19] style: apply MegaLinter's formatter fixes across the repository The organization Go workflow's MegaLinter run formats every file, then commits the result with the workflow token. That token cannot change workflow files, so the commit was refused. These are the same fixes: shfmt -i 2 -ci on shell scripts, prettier on JSON and YAML outside k8s/, and markdownlint. The Coroot security-progress ConfigMap is regenerated because it packages the reformatted healthcheck.sh. Co-Authored-By: Claude Opus 5.5 --- .github/actions/deploy-prod/action.yml | 2 +- .github/renovate.json | 9 +- .../scripts/fetch-cilium-deny-candidate.sh | 5 +- .github/scripts/kyverno-version.sh | 2 +- .github/workflows/cd.yaml | 2 +- .github/workflows/ci.yaml | 6 +- .github/workflows/dr-rebuild.yaml | 2 +- .github/workflows/expand-openbao-audit.yaml | 2 +- .../workflows/prepare-longhorn-v2-node.yaml | 24 +- ...e-publish-workflow-approved-revisions.yaml | 2 +- .github/workflows/validate-main.yaml | 3 +- docs/TENANTS.md | 2 +- docs/adr/headless-cms.md | 2 +- docs/adr/storage-retention.md | 2 +- docs/dr/velero-cnpg.md | 6 +- docs/go-vulnerability-scan.md | 2 +- ...helm-release-ready-after-failed-upgrade.md | 2 +- docs/tenant-abstraction.md | 2 +- .../security-progress/config-map.yaml | 44 +-- .../security-progress/healthcheck.sh | 28 +- scripts/admit-coroot-baseline-context.sh | 22 +- scripts/alert-publisher/README.md | 2 +- scripts/audit-storage-retention/capture.sh | 10 +- scripts/bootstrap-tenant-source.sh | 56 +-- scripts/check-flux-orphaned-objects.sh | 12 +- scripts/check-kubescape-result-coverage.sh | 16 +- scripts/cleanup-orphan-generated-vpas.sh | 28 +- scripts/diagnose-email-startup/probe.sh | 5 +- ...ate-publish-workflow-approved-revisions.sh | 137 +++++-- ...guard-baseline-context-optin-scan-scope.sh | 3 +- .../guard-consumer-discovery-conservation.sh | 6 +- scripts/guard-coroot-baseline-context.sh | 32 +- scripts/guard-ghcr-fanout-component-gate.sh | 15 +- scripts/guard-helm-post-renderers.sh | 20 +- scripts/guard-longhorn-ui-baseline-context.sh | 22 +- scripts/guard-oci-repository-verify.sh | 15 +- ...ard-publish-workflow-approved-revisions.sh | 2 +- scripts/guard-shared-publish-workflow-pin.sh | 4 +- scripts/guard-test-script-exec-bit.sh | 7 +- scripts/judge-openbao-health/README.md | 2 +- scripts/judge-openbao-quorum/README.md | 2 +- scripts/prepare-email-credentials.sh | 28 +- .../inventory-request.json | 111 +++++- scripts/private-mail-config/stage-webui.sh | 19 +- scripts/prove-cilium-default-deny.sh | 41 ++- scripts/prove-kyverno-stale-report-prune.sh | 2 +- ...publish-workflow-approved-revisions.lib.sh | 28 +- scripts/read-actual-budget-userns-maps.sh | 4 +- scripts/read-cilium-api-limiter.sh | 15 +- scripts/reassert-flux-root-verify.sh | 5 +- scripts/refresh-email-recovery-backup.sh | 37 +- scripts/remove-wedding-shared-backup-pod.sh | 2 +- ...port-publish-workflow-signing-revisions.sh | 9 +- scripts/required-merge-workflows.sh | 7 +- scripts/retire-arc-pool.sh | 168 ++++++--- scripts/run-openbao-backup-sandbox.sh | 17 +- scripts/run-publication-generator-tests.sh | 9 +- scripts/tenant-publication-binding.lib.sh | 6 +- scripts/tenant-source-discovery.lib.sh | 9 +- .../lib/configure-hosted-registry-cache.sh | 12 +- scripts/tests/resolve-rendered-chart.sh | 5 +- .../test-actual-budget-user-namespaces.sh | 5 +- scripts/tests/test-add-baseline-context.sh | 1 - .../test-admit-coroot-baseline-context.sh | 67 +++- scripts/tests/test-alert-app-proof.sh | 5 +- .../tests/test-arc-listener-public-content.sh | 5 +- scripts/tests/test-arc-retirement-fence.sh | 9 +- .../test-authorization-gate-diagnostics.sh | 5 +- ...st-auto-vpa-generating-policy-ownership.sh | 10 +- .../tests/test-business-disclosure-reader.sh | 5 +- scripts/tests/test-business-staging-hold.sh | 18 +- .../tests/test-canonical-bootstrap-output.sh | 38 +- .../test-canonical-constructor-attribution.sh | 4 +- .../tests/test-canonical-generator-output.sh | 3 +- .../test-canonical-publisher-regeneration.sh | 75 +++- .../tests/test-canonical-source-bootstrap.sh | 124 ++++--- .../tests/test-canonical-source-upgrade.sh | 110 ++++-- .../tests/test-check-flux-orphaned-objects.sh | 10 +- .../test-cilium-hubble-baseline-context.sh | 5 +- .../test-cleanup-orphan-generated-vpas.sh | 40 +- ...st-coroot-application-health-reconciler.sh | 54 +-- scripts/tests/test-coroot-dns-resolution.sh | 5 +- .../tests/test-coroot-reservation-refresh.sh | 5 +- scripts/tests/test-cronjob-failure-alert.sh | 64 +++- ...st-crossplane-aws-family-version-parity.sh | 12 +- scripts/tests/test-crossplane-sync-alerter.sh | 5 +- .../tests/test-descheduler-availability.sh | 5 +- scripts/tests/test-dex-theme-mount.sh | 70 +++- .../tests/test-dr-longhorn-disk-preflight.sh | 5 +- .../tests/test-fetch-cilium-deny-candidate.sh | 24 +- .../test-frozen-legacy-publisher-release.sh | 40 +- .../tests/test-github-bootstrap-isolation.sh | 6 +- .../test-github-team-identity-admission.sh | 4 +- ...t-guard-cilium-clusterwide-default-deny.sh | 51 ++- ...t-guard-consumer-discovery-conservation.sh | 205 ++++++++--- .../test-guard-coroot-baseline-context.sh | 57 ++- ...t-guard-crossplane-provider-name-parity.sh | 12 +- .../test-guard-ghcr-fanout-component-gate.sh | 11 +- .../tests/test-guard-helm-post-renderers.sh | 4 +- .../tests/test-guard-kubescape-scan-scope.sh | 5 +- .../tests/test-guard-limitrange-premise.sh | 1 - ...test-guard-longhorn-ui-baseline-context.sh | 2 +- .../tests/test-guard-oci-repository-verify.sh | 108 ++++-- .../tests/test-guard-test-script-exec-bit.sh | 27 +- scripts/tests/test-guard-tracked-exec-bit.sh | 112 +++--- .../test-hcloud-baseline-worker-affinity.sh | 25 +- scripts/tests/test-hubble-ui-streaming.sh | 11 +- ...-inventory-first-party-image-signatures.sh | 2 +- scripts/tests/test-kro-renovate-group.sh | 2 +- scripts/tests/test-kubelet-config-readback.sh | 2 +- .../test-kubescape-runtime-rules-switch.sh | 10 +- .../tests/test-kubescape-storage-hotfix.sh | 4 +- .../tests/test-kubescape-storage-placement.sh | 10 +- scripts/tests/test-kyverno-auth-reviews.sh | 10 +- .../tests/test-kyverno-background-hotfix.sh | 19 +- .../test-kyverno-migration-hook-lifecycle.sh | 5 +- .../tests/test-kyverno-migration-runtime.sh | 19 +- .../tests/test-longhorn-client-placement.sh | 8 +- ...st-longhorn-csi-plugin-baseline-context.sh | 6 +- ...-longhorn-engine-image-baseline-context.sh | 6 +- .../test-longhorn-ui-baseline-context.sh | 5 +- scripts/tests/test-merge-group-evicted.sh | 5 +- scripts/tests/test-openbao-standby-canary.sh | 8 +- .../tests/test-openbao-strategy-transition.sh | 5 +- scripts/tests/test-parallel-guard-cleanup.sh | 15 +- .../tests/test-private-mail-configuration.sh | 151 +++++--- scripts/tests/test-private-mail-server.sh | 25 +- .../tests/test-prod-recovery-source-wiring.sh | 120 ++++-- scripts/tests/test-prod-recovery-source.sh | 55 ++- scripts/tests/test-prod-stranded-gate.sh | 5 +- .../tests/test-prove-cilium-default-deny.sh | 41 ++- scripts/tests/test-publication-subject.sh | 5 +- ...blish-workflow-approved-revisions-guard.sh | 55 ++- ...est-publish-workflow-approved-revisions.sh | 82 +++-- ...st-publish-workflow-canonical-approvals.sh | 66 ++-- ...ish-workflow-signing-revision-resolvers.sh | 45 ++- ...test-publish-workflow-signing-revisions.sh | 24 +- scripts/tests/test-read-cilium-api-limiter.sh | 79 ++-- .../tests/test-redeploy-main-when-behind.sh | 5 +- ...ate-publish-workflow-approved-revisions.sh | 342 +++++++++++++----- .../test-reloader-placement-selection.sh | 5 +- .../tests/test-reloader-rendered-placement.sh | 5 +- scripts/tests/test-report-prod-convergence.sh | 6 +- .../tests/test-required-merge-workflows.sh | 43 ++- .../test-restore-stranded-prod-workflow.sh | 80 +++- .../test-restrict-tenant-network-policies.sh | 10 +- scripts/tests/test-retire-arc-pool.sh | 205 ++++++++--- .../test-rgd-template-static-scan-wiring.sh | 20 +- .../test-run-publication-generator-tests.sh | 48 +-- .../tests/test-setup-supply-chain-tools.sh | 5 +- .../test-shared-publish-workflow-pin-guard.sh | 1 - .../test-signing-report-publisher-families.sh | 20 +- .../test-stranded-autoscaler-node-alerter.sh | 8 +- .../test-stranded-volume-attach-alert.sh | 2 +- .../tests/test-talos-render-kernel-args.sh | 65 +++- .../tests/test-talos-schematic-readback.sh | 2 +- .../test-tenant-edit-networkpolicy-write.sh | 10 +- scripts/tests/test-tenant-kro-pull-secret.sh | 3 +- .../tests/test-tenant-source-constructors.sh | 2 +- scripts/tests/test-tenant-source-discovery.sh | 5 +- .../tests/test-umami-rendered-placement.sh | 5 +- scripts/tests/test-umami-tracking-probe.sh | 6 +- ...lidate-image-verifier-liveness-workflow.sh | 50 ++- .../tests/test-velero-cancellation-release.sh | 5 +- .../test-velero-maintenance-oom-alert.sh | 5 +- .../tests/test-velero-node-agent-priority.sh | 5 +- scripts/tests/test-verify-arc-transport.sh | 99 +++-- .../test-verify-wedding-backup-cutover.sh | 31 +- .../test-verify-wedding-backup-denial.sh | 21 +- ...t-verify-wedding-shared-backup-coverage.sh | 3 +- scripts/tests/test-webapp-compiled-scan.sh | 15 +- scripts/tests/test-webapp-compiler-wiring.sh | 11 +- scripts/tests/test-webapp-compiler.sh | 34 +- .../tests/test-webapp-container-security.sh | 64 ++-- scripts/tests/test-webapp-inputs-wiring.sh | 21 +- scripts/tests/test-webapp-runtime-hosted.sh | 21 +- scripts/tests/test-world-zone-signatures.sh | 5 +- scripts/tests/test-world-zone-trial.sh | 55 ++- .../test-write-publish-workflow-matchers.sh | 15 +- scripts/validate-homepage-bookmarks.sh | 5 +- scripts/validate-image-verifier-liveness.sh | 10 +- scripts/validate-webapp-inputs.sh | 2 +- scripts/verify-arc-transport.sh | 45 ++- scripts/verify-email-provider/run.sh | 9 +- scripts/verify-heartbeat-cleanup.sh | 9 +- scripts/verify-image-unsigned.sh | 9 +- scripts/verify-kubelet-config-readback.sh | 6 +- .../verify-private-mail-storage-recovery.sh | 2 +- scripts/verify-prod-recovery-source.sh | 12 +- scripts/verify-shared-backup-consumers.sh | 7 +- scripts/verify-talos-schematic-readback.sh | 12 +- scripts/verify-wedding-backup-cutover.sh | 9 +- .../verify-wedding-shared-backup-coverage.sh | 2 +- .../patched-resources.yaml | 65 ++-- .../unevaluated-namespace-fixtures.yaml | 9 +- .../resources.yaml | 1 - .../kyverno-test.yaml | 1 - .../resources.yaml | 5 +- .../values.yaml | 1 - .../kyverno-test.yaml | 1 - .../values.yaml | 1 - .../resources.yaml | 2 +- .../resources.yaml | 4 +- 203 files changed, 3523 insertions(+), 1533 deletions(-) diff --git a/.github/actions/deploy-prod/action.yml b/.github/actions/deploy-prod/action.yml index 3a0f8ef45e..8dbb9ec150 100644 --- a/.github/actions/deploy-prod/action.yml +++ b/.github/actions/deploy-prod/action.yml @@ -59,7 +59,7 @@ runs: - name: 🔐 Enforce approved publication signers shell: bash env: - APPROVED_REVISIONS_ENFORCE: '1' + APPROVED_REVISIONS_ENFORCE: "1" run: ./scripts/guard-publish-workflow-approved-revisions.sh # Keep signer approval ahead of credentials, then refuse stale recovery diff --git a/.github/renovate.json b/.github/renovate.json index 44c3eaa611..c0d8cd17e0 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -248,7 +248,9 @@ }, { "description": "The flux-operator chart (dedicated regex manager above) installs and upgrades Flux itself. Its chart lives on ghcr.io, which gives Renovate no release date for it, and a release with no date never clears the repository-wide wait, so no update was proposed after 0.50.0 (#4985, the same cause as #4754). minimumReleaseAge is therefore 0 days. Raising the PR adopts nothing: automerge is off whatever the update type, so every update is reviewed against the operator's release notes and the HelmRelease post-renderers before merge. Placed after the broad automerge rules so none of them can turn automerge back on.", - "matchPackageNames": ["ghcr.io/controlplaneio-fluxcd/charts/flux-operator"], + "matchPackageNames": [ + "ghcr.io/controlplaneio-fluxcd/charts/flux-operator" + ], "minimumReleaseAge": "0 days", "automerge": false }, @@ -392,7 +394,10 @@ { "description": "The Kopia reader (internal/backupcatalogue) replaces github.com/kopia/kopia with the exact project-velero/kopia commit the deployed Velero's own go.mod pins, so it reads the backup format production writes. That fork is pinned by pseudo-version, and semver ranks every v0.0.0 pseudo-version below every tag, so Renovate offers the older v0.19.0-velero.1 (February 2025, 390 commits behind the pin, December 2025) as a minor update with automerge on. Only a compile error stops that downgrade. github.com/kopia/kopia is covered too: the replacement means its require version is never the code we build, and Velero carries v0.16.0 beside this same replacement, so a bump would change nothing we run and make go.mod say something untrue. The pin moves with Velero instead: when a Velero bump changes the fork commit in its go.mod, change it here in one reviewed change. scripts/tests/test-velero-kopia-renovate-pin.sh fails once the replacement is no longer a pseudo-version, which is when this rule can go.", "matchManagers": ["gomod"], - "matchPackageNames": ["github.com/kopia/kopia", "github.com/project-velero/kopia"], + "matchPackageNames": [ + "github.com/kopia/kopia", + "github.com/project-velero/kopia" + ], "enabled": false } ], diff --git a/.github/scripts/fetch-cilium-deny-candidate.sh b/.github/scripts/fetch-cilium-deny-candidate.sh index 9c4ce7cb59..f6d1f91455 100755 --- a/.github/scripts/fetch-cilium-deny-candidate.sh +++ b/.github/scripts/fetch-cilium-deny-candidate.sh @@ -3,7 +3,10 @@ # Candidate scripts, workflows and checkout code are never executed. set -euo pipefail umask 077 -fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} [[ "${CANDIDATE_PR:-}" =~ ^[1-9][0-9]*$ && "${CANDIDATE_HEAD:-}" =~ ^[0-9a-f]{40}$ ]] || fail 'candidate PR and exact SHA required' [[ "${RUNNER_TEMP:-}" == /* && -d "$RUNNER_TEMP" ]] || fail 'absolute runner scratch directory required' readonly candidate="${RUNNER_TEMP}/cilium-deny-candidate" diff --git a/.github/scripts/kyverno-version.sh b/.github/scripts/kyverno-version.sh index 9863892a72..f151b2d85a 100755 --- a/.github/scripts/kyverno-version.sh +++ b/.github/scripts/kyverno-version.sh @@ -6,4 +6,4 @@ set -euo pipefail KYVERNO_VERSION="1.19.2" : "${GITHUB_OUTPUT:?GitHub Actions output file required}" -printf 'release=v%s\n' "$KYVERNO_VERSION" >> "$GITHUB_OUTPUT" +printf 'release=v%s\n' "$KYVERNO_VERSION" >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 2f1020a015..e8afcd0480 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -150,7 +150,7 @@ jobs: - name: 🔐 Enforce approved publication signers env: - APPROVED_REVISIONS_ENFORCE: '1' + APPROVED_REVISIONS_ENFORCE: "1" run: ./scripts/guard-publish-workflow-approved-revisions.sh - name: ⚙️ Setup Go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ced492ef9..f583deec9b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2021,7 +2021,7 @@ jobs: if: matrix.suite == 'services' timeout-minutes: 5 env: - CONTRACT_PROBE_IMAGE_NATIVE_TEST: '1' + CONTRACT_PROBE_IMAGE_NATIVE_TEST: "1" run: | set -euo pipefail go test -count=1 -timeout 4m -json ./scripts/tests/contract-probe-image | @@ -3568,13 +3568,13 @@ jobs: if: matrix.suite == 'native' timeout-minutes: 5 env: - VAULT_BACKUP_TOOLING_NATIVE_TEST: '1' + VAULT_BACKUP_TOOLING_NATIVE_TEST: "1" run: go test -count=1 -v ./scripts/tests/vault-backup-recovery - name: 🔒 Prove Longhorn dashboard read-only routing if: matrix.suite == 'native' timeout-minutes: 5 env: - LONGHORN_UI_NATIVE_TEST: '1' + LONGHORN_UI_NATIVE_TEST: "1" run: go test -race -count=1 -v ./scripts/tests/longhorn-ui-readonly - name: 🐤 Validate staged OpenBao standby repair if: matrix.suite == 'contracts' diff --git a/.github/workflows/dr-rebuild.yaml b/.github/workflows/dr-rebuild.yaml index 87e2d83408..c96749431a 100644 --- a/.github/workflows/dr-rebuild.yaml +++ b/.github/workflows/dr-rebuild.yaml @@ -112,7 +112,7 @@ jobs: - name: 🔐 Enforce approved publication signers env: - APPROVED_REVISIONS_ENFORCE: '1' + APPROVED_REVISIONS_ENFORCE: "1" run: ./scripts/guard-publish-workflow-approved-revisions.sh - name: 🛡️ Protect the nested RGD deployment gate diff --git a/.github/workflows/expand-openbao-audit.yaml b/.github/workflows/expand-openbao-audit.yaml index 791362d0bc..cf4a2c299c 100644 --- a/.github/workflows/expand-openbao-audit.yaml +++ b/.github/workflows/expand-openbao-audit.yaml @@ -80,7 +80,7 @@ jobs: - name: Require offline preservation and real patch transport controls env: - AUDIT_REQUIRE_NATIVE_CONTROL: '1' + AUDIT_REQUIRE_NATIVE_CONTROL: "1" run: go test -race -count=1 ./scripts/expand-openbao-audit - name: Restore the established protected production kubeconfig diff --git a/.github/workflows/prepare-longhorn-v2-node.yaml b/.github/workflows/prepare-longhorn-v2-node.yaml index 49a0d1a773..9f48e6452d 100644 --- a/.github/workflows/prepare-longhorn-v2-node.yaml +++ b/.github/workflows/prepare-longhorn-v2-node.yaml @@ -18,12 +18,32 @@ name: Prepare Longhorn V2 Migration description: The guarded migration step to run required: true type: choice - options: [prepare-worker, prepare-unused-temporary-worker, refresh-clients, benchmark-v1, benchmark-v2, inventory-replicas, inventory-storage-root, inventory-storage-layout, inspect-host-memory, reboot-host-for-hugepages] + options: + [ + prepare-worker, + prepare-unused-temporary-worker, + refresh-clients, + benchmark-v1, + benchmark-v2, + inventory-replicas, + inventory-storage-root, + inventory-storage-layout, + inspect-host-memory, + reboot-host-for-hugepages, + ] node: description: Permanent worker or fixed temporary-worker selection for the chosen operation required: false type: choice - options: [prod-worker-1, prod-worker-2, prod-worker-3, temporary-v1-1, temporary-v1-2, temporary-v2-1] + options: + [ + prod-worker-1, + prod-worker-2, + prod-worker-3, + temporary-v1-1, + temporary-v1-2, + temporary-v2-1, + ] confirm: description: "Type the exact confirmation documented for the selected operation" required: true diff --git a/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml b/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml index 579c9c7244..af9f4c331c 100644 --- a/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml +++ b/.github/workflows/regenerate-publish-workflow-approved-revisions.yaml @@ -204,7 +204,7 @@ jobs: if: steps.moved.outputs.moved == 'true' shell: bash env: - APPROVED_REVISIONS_ENFORCE: '1' + APPROVED_REVISIONS_ENFORCE: "1" run: ./scripts/guard-publish-workflow-approved-revisions.sh - name: 📦 Open or update the regeneration pull request diff --git a/.github/workflows/validate-main.yaml b/.github/workflows/validate-main.yaml index ecf15784db..855c9156df 100644 --- a/.github/workflows/validate-main.yaml +++ b/.github/workflows/validate-main.yaml @@ -98,7 +98,7 @@ jobs: - name: 🔐 Enforce approved publication signers env: - APPROVED_REVISIONS_ENFORCE: '1' + APPROVED_REVISIONS_ENFORCE: "1" run: ./scripts/guard-publish-workflow-approved-revisions.sh - name: ⚙️ Setup Go @@ -208,7 +208,6 @@ jobs: set -o pipefail ./scripts/report-publish-workflow-signing-revisions.sh | tee -a "$GITHUB_STEP_SUMMARY" - # ci.yaml's validate-talos job runs the Kubernetes/Talos compatibility predicate on # `pull_request` and `merge_group`, but neither event fires for a direct push to main. # Such a push could otherwise reach the manually dispatched CD route unchecked. diff --git a/docs/TENANTS.md b/docs/TENANTS.md index 71d7d29d4e..fb7029b13c 100644 --- a/docs/TENANTS.md +++ b/docs/TENANTS.md @@ -163,7 +163,7 @@ WebApp, external DNS and SOPS disabled. Fetching that source does not deploy the portal or prove any image/identity/private-input readiness. | Verifier | Artifact | What it accepts | -|---|---|---| +| --- | --- | --- | | **Flux** — a registered tenant's `OCIRepository` | manifest | Its recorded legacy signer, current pin and release candidate, plus any exact maintained revisions explicitly approved for that consumer. Other commits are refused. | | **Admission** — the `verify-app-images` Kyverno policy | image | Legacy `actions` publishers with a full commit identity, or the explicitly reviewed maintained `.github` publisher revision, from the GitHub OIDC issuer. Other maintained commits, tags and branches are refused. | | **Node pull** — the Talos image-verification rule | image | The same legacy compatibility and exact maintained revision as admission. | diff --git a/docs/adr/headless-cms.md b/docs/adr/headless-cms.md index 3c555ee8e6..a7cc3156a0 100644 --- a/docs/adr/headless-cms.md +++ b/docs/adr/headless-cms.md @@ -62,7 +62,7 @@ need to change. ✅ meets · ⚠️ partly, or with our own work · ❌ fails · — not assessed after a hard-filter failure. | Candidate | OSI licence | Popular and maintained | Simple to run and use | Cloud-native self-hosting | Fits our sites | Own logins, per-client isolation | Platform guardrails | -|---|---|---|---|---|---|---|---| +| --- | --- | --- | --- | --- | --- | --- | --- | | **Payload** | ✅ MIT. SSO and some workflow features are a paid add-on we do not need | ✅ ~45k stars, ~1M npm downloads a week, weekly releases | ✅ one Next.js process plus Postgres; polished editor | ⚠️ no official image, so we build it; Postgres and S3 adapters | ✅ REST and GraphQL, framework-agnostic live preview, built-in localisation | ✅ built-in users and access control; one instance per client | ✅ our signed image; no exception expected | | **Strapi** | ⚠️ MIT except `ee/`; live preview, content history and SSO need paid plans | ✅ ~73k stars, weekly releases | ⚠️ documented host minimum of 2 GB, 4 GB recommended, which sizes a whole server with the build; Payload publishes no figure, so increment 2 measures both | ⚠️ no official image; Postgres; S3 provider | ✅ REST and GraphQL; free localisation | ✅ built-in users and roles; one instance per client | ✅ our signed image, as for Payload | | **Directus** | ❌ MSCL since v12 (June 2026), with enforced limits on the free tier | ✅ ~38k stars, monthly releases | ✅ | ✅ official amd64 and arm64 image | ✅ | ✅ | — | diff --git a/docs/adr/storage-retention.md b/docs/adr/storage-retention.md index eeb97a204c..67b1214fd3 100644 --- a/docs/adr/storage-retention.md +++ b/docs/adr/storage-retention.md @@ -115,7 +115,7 @@ removed. `Retain` lets the deletion succeed. ### Narrow the annotation to stateful resources — rejected -#3368 kept the annotation on claims and releases and took it off namespaces. A narrower opt-out is +# 3368 kept the annotation on claims and releases and took it off namespaces. A narrower opt-out is still an opt-out: whatever keeps the annotation is still outside declarative management. Any rule that treats stateless tenants differently also needs to know which tenants are stateless, and that cannot be read from this repository. diff --git a/docs/dr/velero-cnpg.md b/docs/dr/velero-cnpg.md index 075993a5e4..a28a6053f1 100644 --- a/docs/dr/velero-cnpg.md +++ b/docs/dr/velero-cnpg.md @@ -98,12 +98,12 @@ Backups always land in R2 as a Kopia repository, so they are portable for cross-provider/cross-distribution restore regardless of how the volume was captured. *How* each volume is captured depends on its storage backend: -| Storage backend | Method | Why | +| Storage backend | Method | Why | | ---------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------ | -| Longhorn (`longhorn` SC) | CSI snapshot → Kopia data mover → R2 | Crash-consistent; also backs up PVCs of scaled-to-zero apps (no running pod needed). | +| Longhorn (`longhorn` SC) | CSI snapshot → Kopia data mover → R2 | Crash-consistent; also backs up PVCs of scaled-to-zero apps (no running pod needed). | | hcloud + `backup.platform.devantler.tech/volume-data=independently-mirrored` (`openbao/vault-snapshots` only) | Skipped by Velero; the OpenBao CronJob mirrors raft snapshots directly to R2 | The PVC is unmounted during Velero's window, so FSB cannot read it, and Hetzner block storage has no CSI snapshot support. | | other hcloud claims (OpenBao data/audit and Coroot telemetry) | File-system backup (Kopia) → R2 | hcloud-csi has no snapshot support; mounted controller-created claims use the fail-safe FSB path. The copy is file-level and taken while ClickHouse runs, so Coroot telemetry restores best-effort only ([#3673](https://github.com/devantler-tech/platform/issues/3673)). | -| anything else / new PVCs | File-system backup (Kopia) → R2 | Fail-safe default. | +| anything else / new PVCs | File-system backup (Kopia) → R2 | Fail-safe default. | The routing is declarative through storage attributes, not per-pod annotations: diff --git a/docs/go-vulnerability-scan.md b/docs/go-vulnerability-scan.md index 986e2254af..5b0f312a09 100644 --- a/docs/go-vulnerability-scan.md +++ b/docs/go-vulnerability-scan.md @@ -18,7 +18,7 @@ function, not merely when it depends on the affected package. lists every tracked Go module once, with one of four modes: | Mode | Meaning | -|---|---| +| --- | --- | | `scan` | Reachable advisories fail the check. The module must have a non-test Go file, and the scan's own report must show that it analysed at least one package of the module with Go code; a scan that looked at nothing fails rather than passing as clean. | | `negative-control` | The scan must report reachable advisories. The fixture pins an old Go release and calls affected standard-library functions, so a run that reports nothing proves the scanner is broken, not that the code is clean. A negative control must be a fixture under `scripts/tests/fixtures/`. | | `test-only` | Not scanned, because the module holds only tests and the shared scan action does not analyse tests (it does not pass `-test`). The check fails as soon as the module gains a non-test Go file. | diff --git a/docs/operations/helm-release-ready-after-failed-upgrade.md b/docs/operations/helm-release-ready-after-failed-upgrade.md index 323e6245a0..921e5cef49 100644 --- a/docs/operations/helm-release-ready-after-failed-upgrade.md +++ b/docs/operations/helm-release-ready-after-failed-upgrade.md @@ -24,7 +24,7 @@ version production runs, with Flux 2.9) and on its `main` branch on 2026-10-07: ## What tells you | Signal | Lasts | Where | -|---|---|---| +| --- | --- | --- | | The Flux reconciliation alert | One message, at the moment of the failure | Slack, from [`alert.yaml`](../../k8s/providers/hetzner/infrastructure/flux-notifications/alert.yaml); it carried the full error text on 2026-09-04 | | A Warning `UpgradeFailed` event on the release | As long as the API server keeps events, about an hour | `kubectl get events --namespace `; events are namespaced, so the bare command reads the wrong place | | The release's own failure count | Until its chart version, values or generation next change | `status.failures` (`status.upgradeFailures` stays at zero, because the failed attempt stored no release) | diff --git a/docs/tenant-abstraction.md b/docs/tenant-abstraction.md index ed3889f1d5..2b3812b213 100644 --- a/docs/tenant-abstraction.md +++ b/docs/tenant-abstraction.md @@ -204,7 +204,7 @@ Reserved policy names do not inherit an exception for an already noncompliant old object: platform creation is not permission for a tenant's later update. This ownership boundary is separate from the hold's actual traffic effect. Routing, projection and live hold/serve/isolate/recover acceptance remain under -#5247 before exposure; the source-only baseline does not enable an application. +# 5247 before exposure; the source-only baseline does not enable an application. ### Public business-site configuration identity diff --git a/k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml b/k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml index 4ea911fc33..06e214b3e5 100644 --- a/k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml +++ b/k8s/bases/infrastructure/coroot/components/security-progress/config-map.yaml @@ -108,14 +108,14 @@ data: while [ "$#" -gt 0 ]; do case "$1" in - --context) need_value "$@" && context="$2" && shift 2 ;; - --cache-dir) need_value "$@" && cache_dir="$2" && shift 2 ;; - --from-dir) need_value "$@" && from_dir="$2" && shift 2 ;; - --snapshot-dir) need_value "$@" && snapshot_dir="$2" && shift 2 ;; - --now) need_value "$@" && now="$2" && shift 2 ;; - --vuln-max-age-days) need_value "$@" && vuln_max_age_days="$2" && shift 2 ;; - --reviewed-list) need_value "$@" && reviewed="$2" && shift 2 ;; - *) die "unknown argument: $1" ;; + --context) need_value "$@" && context="$2" && shift 2 ;; + --cache-dir) need_value "$@" && cache_dir="$2" && shift 2 ;; + --from-dir) need_value "$@" && from_dir="$2" && shift 2 ;; + --snapshot-dir) need_value "$@" && snapshot_dir="$2" && shift 2 ;; + --now) need_value "$@" && now="$2" && shift 2 ;; + --vuln-max-age-days) need_value "$@" && vuln_max_age_days="$2" && shift 2 ;; + --reviewed-list) need_value "$@" && reviewed="$2" && shift 2 ;; + *) die "unknown argument: $1" ;; esac done @@ -1634,12 +1634,12 @@ data: require_project=false case "$${1:-}" in - "") [[ $# == 0 ]] || exit 2 ;; - --require-project) - [[ $# == 1 ]] || exit 2 - require_project=true - ;; - *) exit 2 ;; + "") [[ $# == 0 ]] || exit 2 ;; + --require-project) + [[ $# == 1 ]] || exit 2 + require_project=true + ;; + *) exit 2 ;; esac metrics_file=$${METRICS_FILE:-/metrics/metrics} @@ -1670,14 +1670,14 @@ data: [[ -z "$project_id" || "$project_id" == "$current_project" ]] || exit 1 project_id=$current_project case "$surface" in - policy) - "$policy_seen" && exit 1 - policy_seen=true - ;; - kubescape) - "$kubescape_seen" && exit 1 - kubescape_seen=true - ;; + policy) + "$policy_seen" && exit 1 + policy_seen=true + ;; + kubescape) + "$kubescape_seen" && exit 1 + kubescape_seen=true + ;; esac done <"$metrics_file" "$policy_seen" && "$kubescape_seen" diff --git a/k8s/bases/infrastructure/coroot/components/security-progress/healthcheck.sh b/k8s/bases/infrastructure/coroot/components/security-progress/healthcheck.sh index 04008ad23d..4b936fb81f 100644 --- a/k8s/bases/infrastructure/coroot/components/security-progress/healthcheck.sh +++ b/k8s/bases/infrastructure/coroot/components/security-progress/healthcheck.sh @@ -4,12 +4,12 @@ set -euo pipefail require_project=false case "${1:-}" in -"") [[ $# == 0 ]] || exit 2 ;; ---require-project) - [[ $# == 1 ]] || exit 2 - require_project=true - ;; -*) exit 2 ;; + "") [[ $# == 0 ]] || exit 2 ;; + --require-project) + [[ $# == 1 ]] || exit 2 + require_project=true + ;; + *) exit 2 ;; esac metrics_file=${METRICS_FILE:-/metrics/metrics} @@ -40,14 +40,14 @@ while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$project_id" || "$project_id" == "$current_project" ]] || exit 1 project_id=$current_project case "$surface" in - policy) - "$policy_seen" && exit 1 - policy_seen=true - ;; - kubescape) - "$kubescape_seen" && exit 1 - kubescape_seen=true - ;; + policy) + "$policy_seen" && exit 1 + policy_seen=true + ;; + kubescape) + "$kubescape_seen" && exit 1 + kubescape_seen=true + ;; esac done <"$metrics_file" "$policy_seen" && "$kubescape_seen" diff --git a/scripts/admit-coroot-baseline-context.sh b/scripts/admit-coroot-baseline-context.sh index 10070c5548..ea917436e8 100755 --- a/scripts/admit-coroot-baseline-context.sh +++ b/scripts/admit-coroot-baseline-context.sh @@ -14,14 +14,26 @@ context=admin@prod rollout_timeout=15m while [[ $# -gt 0 ]]; do case "$1" in - --context) context="${2:?context required}"; shift 2 ;; - --rollout-timeout) rollout_timeout="${2:?timeout required}"; shift 2 ;; - *) printf 'Unknown argument: %s\n' "$1" >&2; exit 2 ;; + --context) + context="${2:?context required}" + shift 2 + ;; + --rollout-timeout) + rollout_timeout="${2:?timeout required}" + shift 2 + ;; + *) + printf 'Unknown argument: %s\n' "$1" >&2 + exit 2 + ;; esac done scratch="$(mktemp -d)" trap 'rm -rf "${scratch}"' EXIT -fail() { printf '::error::Coroot baseline admission: %s\n' "$1" >&2; exit 1; } +fail() { + printf '::error::Coroot baseline admission: %s\n' "$1" >&2 + exit 1 +} # The reviewed population, identical to guard-coroot-baseline-context.sh. Every # read must match it exactly, so no write ever reaches a template outside it. @@ -73,7 +85,7 @@ for ((attempt = 1; attempt <= 3; attempt++)); do lacking >"${scratch}/still" [[ -s "${scratch}/still" ]] || break sort -u "${scratch}/written" "${scratch}/still" -o "${scratch}/written" - if (( attempt < 3 )); then sleep 10; fi + if ((attempt < 3)); then sleep 10; fi done [[ ! -s "${scratch}/still" ]] || fail "admission did not add both fields to: $(paste -sd ' ' "${scratch}/still")" diff --git a/scripts/alert-publisher/README.md b/scripts/alert-publisher/README.md index d22fd66eaf..8467732192 100644 --- a/scripts/alert-publisher/README.md +++ b/scripts/alert-publisher/README.md @@ -87,7 +87,7 @@ route. App or shared-credential retirement follows verified consumer migration. ## Cluster-independent journal preparation The optional `r2` backend prepares the independent journal needed by #5328 and -#4683. It uses the maintained AWS S3 SDK with explicit static credentials, +# 4683. It uses the maintained AWS S3 SDK with explicit static credentials, the configured account's global Cloudflare R2 S3 endpoint, region `auto`, bounded requests, no redirects and no automatic request retries. It does not discover credentials from the environment or create a bucket. The existing diff --git a/scripts/audit-storage-retention/capture.sh b/scripts/audit-storage-retention/capture.sh index a30fd990a6..6f2062956c 100644 --- a/scripts/audit-storage-retention/capture.sh +++ b/scripts/audit-storage-retention/capture.sh @@ -13,9 +13,9 @@ mkdir -- "$directory" started=$(date -u +%Y-%m-%dT%H:%M:%SZ) # Ordinary kubectl JSON flattens typed lists and loses their list resourceVersion. # Read the fixed API endpoints directly so the auditor can verify completeness. -kubectl --context "$context" --request-timeout=30s get --raw /apis/storage.k8s.io/v1/storageclasses > "$directory/classes.json" -kubectl --context "$context" --request-timeout=30s get --raw /api/v1/persistentvolumes > "$directory/volumes.json" -kubectl --context "$context" --request-timeout=30s get --raw /api/v1/persistentvolumeclaims > "$directory/claims.json" -kubectl --context "$context" --request-timeout=30s get --raw /apis/velero.io/v2alpha1/datauploads > "$directory/uploads.json" +kubectl --context "$context" --request-timeout=30s get --raw /apis/storage.k8s.io/v1/storageclasses >"$directory/classes.json" +kubectl --context "$context" --request-timeout=30s get --raw /api/v1/persistentvolumes >"$directory/volumes.json" +kubectl --context "$context" --request-timeout=30s get --raw /api/v1/persistentvolumeclaims >"$directory/claims.json" +kubectl --context "$context" --request-timeout=30s get --raw /apis/velero.io/v2alpha1/datauploads >"$directory/uploads.json" completed=$(date -u +%Y-%m-%dT%H:%M:%SZ) -printf '{"startedAt":"%s","completedAt":"%s"}\n' "$started" "$completed" > "$directory/capture.json" +printf '{"startedAt":"%s","completedAt":"%s"}\n' "$started" "$completed" >"$directory/capture.json" diff --git a/scripts/bootstrap-tenant-source.sh b/scripts/bootstrap-tenant-source.sh index 03156b3967..0e09647858 100755 --- a/scripts/bootstrap-tenant-source.sh +++ b/scripts/bootstrap-tenant-source.sh @@ -33,14 +33,18 @@ canonical_bootstrap_admission() ( [ ! -L "$output" ] || return 1 [ -z "${CANONICAL_TENANT_BINDINGS_FILE:-}" ] || [ "$CANONICAL_TENANT_BINDINGS_FILE" = "$output" ] || return 2 [ ! -e "$output" ] || previous_exists=1 - previous="$(unset CANONICAL_TENANT_BINDINGS_FILE; canonical_tenant_bindings "$admission_root")" || return 1 + previous="$( + unset CANONICAL_TENANT_BINDINGS_FILE + canonical_tenant_bindings "$admission_root" + )" || return 1 if [ "$previous_exists" = 1 ] && [ "$(printf '%s\n' "$previous" | jq '.bindings|length')" = 0 ]; then echo 'existing empty registry is not an admission; refusing to claim unchanged output' >&2 return 1 fi if [ "$(printf '%s\n' "$previous" | jq '.bindings|length')" -gt 0 ]; then [ "$(printf '%s\n' "$previous" | jq -r '.bindings[0].kind')" = published-bootstrap ] || { - echo 'canonical source already observed; use ordinary regeneration' >&2; return 1; + echo 'canonical source already observed; use ordinary regeneration' >&2 + return 1 } fi receipt="$(business_site_manifest_evidence --consumer business-site --verify-only --tag "$tag" --source-sha "$source_sha" --digest "$digest")" || return 1 @@ -73,7 +77,11 @@ canonical_bootstrap_admission() ( CANONICAL_TENANT_BINDINGS_FILE="$canonical_bootstrap_work/candidate.json" discover_consumers "$admission_root" >/dev/null || return 1 kubectl --context "$PUBLISH_KUBE_CONTEXT" get ocirepositories.source.toolkit.fluxcd.io -A -o json >"$canonical_bootstrap_work/inventory.json" || return 1 state="$(canonical_source_inventory_state "$record" "$canonical_bootstrap_work/inventory.json")" || return 1 - case "$state" in absent | pending) ;; *) echo 'canonical source already has an artifact; use observed regeneration' >&2; return 1 ;; esac + case "$state" in absent | pending) ;; *) + echo 'canonical source already has an artifact; use observed regeneration' >&2 + return 1 + ;; + esac # An existing identical admission is a revalidated no-op. Never replace a # concurrently changed/observed record, or move a temporary file into a # destination that became a directory. @@ -117,7 +125,8 @@ canonical_upgrade_preparation() ( [[ "$tag" =~ ^v[0-9]+[.][0-9]+[.][0-9]+$ ]] && is_sha "$source_sha" && is_digest "$digest" || return 2 [ -n "${PUBLISH_KUBE_CONTEXT:-}" ] && [[ "$upgrade_output" = /* ]] || return 2 local output_parent output_name upgrade_tenant graph_path - output_parent="$(dirname "$upgrade_output")"; output_name="$(basename "$upgrade_output")" + output_parent="$(dirname "$upgrade_output")" + output_name="$(basename "$upgrade_output")" [[ "$output_name" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || return 2 output_parent="$(cd "$output_parent" && pwd -P)" || return 2 upgrade_output="$output_parent/$output_name" @@ -179,7 +188,10 @@ canonical_upgrade_preparation() ( discover_consumers "$upgrade_root" >/dev/null || return 1 # Reserve a new private output directory only after every input has been validated. # Exclusive links cannot overwrite a conflicting file or follow its final symlink. - (umask 077; mkdir "$upgrade_output") || return 1 + ( + umask 077 + mkdir "$upgrade_output" + ) || return 1 mkdir -p "$upgrade_output/scripts" "$upgrade_output/$(dirname "$upgrade_tenant")" || return 1 link "$prepared_root/scripts/canonical-tenant-source-bindings.json" "$upgrade_output/scripts/canonical-tenant-source-bindings.json" || return 1 link "$prepared_root/$upgrade_tenant" "$upgrade_output/$upgrade_tenant" || return 1 @@ -211,14 +223,14 @@ bootstrap_tenant_source() ( return 2 } case "$1" in - --source-file) source_file="$2" ;; - --tag) tag="$2" ;; - --source-root) source_root="$2" ;; - --output) output="$2" ;; - *) - echo 'unknown bootstrap argument' >&2 - return 2 - ;; + --source-file) source_file="$2" ;; + --tag) tag="$2" ;; + --source-root) source_root="$2" ;; + --output) output="$2" ;; + *) + echo 'unknown bootstrap argument' >&2 + return 2 + ;; esac shift 2 done @@ -272,23 +284,23 @@ bootstrap_tenant_source() ( } count="$(printf '%s\n' "$inventory" | jq '[.items[] | select(.spec.url == "oci://ghcr.io/devantler-tech/data-product-controller/manifests")] | length')" case "$count" in - 0) ;; - 1) - printf '%s\n' "$inventory" | jq -e --arg digest "$digest" ' + 0) ;; + 1) + printf '%s\n' "$inventory" | jq -e --arg digest "$digest" ' [.items[] | select(.spec.url == "oci://ghcr.io/devantler-tech/data-product-controller/manifests")][0] | .metadata.namespace == "data-product-controller" and .metadata.name == "khoist-documentation" and .spec.ref.digest == $digest and ((.status.artifact.revision // "") == "") and all(.status.conditions[]?; .type != "Ready" or .status != "True") ' >/dev/null || { + rm -rf "$work" + return 1 + } + ;; + *) rm -rf "$work" return 1 - } - ;; - *) - rm -rf "$work" - return 1 - ;; + ;; esac source_sha="$(tag_commit data-product-controller "$tag")" || { rm -rf "$work" diff --git a/scripts/check-flux-orphaned-objects.sh b/scripts/check-flux-orphaned-objects.sh index 3f82d25b65..b7fd392477 100755 --- a/scripts/check-flux-orphaned-objects.sh +++ b/scripts/check-flux-orphaned-objects.sh @@ -151,9 +151,9 @@ read_projected() { shift 2 kubectl_cluster "$@" 2>"${tmp_dir}/error.log" | jq "${projection}" >"${output}" 2>"${tmp_dir}/projection-error.log" || { - cat "${tmp_dir}/projection-error.log" >>"${tmp_dir}/error.log" - return 1 - } + cat "${tmp_dir}/projection-error.log" >>"${tmp_dir}/error.log" + return 1 + } } # The public job log gets the reason a read failed, without the endpoints and @@ -257,9 +257,9 @@ read_state() { ($adopted_annotation): $m.annotations[$adopted_annotation]}, managedFields: [$m.managedFields[]? | {manager, operation, subresource, time}] }}]} end' >"${dir}/objects.json" 2>"${tmp_dir}/projection-error.log" || { - cat "${tmp_dir}/projection-error.log" >>"${tmp_dir}/error.log" - return 1 - } + cat "${tmp_dir}/projection-error.log" >>"${tmp_dir}/error.log" + return 1 + } # Separate inventory and source reads need no annotations, last-applied # documents, managed field payloads, unrelated spec fields or status messages. read_projected "${dir}/kustomizations.json" ' diff --git a/scripts/check-kubescape-result-coverage.sh b/scripts/check-kubescape-result-coverage.sh index 2103217934..f500302b7c 100755 --- a/scripts/check-kubescape-result-coverage.sh +++ b/scripts/check-kubescape-result-coverage.sh @@ -67,14 +67,14 @@ need_value() { while [ "$#" -gt 0 ]; do case "$1" in - --context) need_value "$@" && context="$2" && shift 2 ;; - --cache-dir) need_value "$@" && cache_dir="$2" && shift 2 ;; - --from-dir) need_value "$@" && from_dir="$2" && shift 2 ;; - --snapshot-dir) need_value "$@" && snapshot_dir="$2" && shift 2 ;; - --now) need_value "$@" && now="$2" && shift 2 ;; - --vuln-max-age-days) need_value "$@" && vuln_max_age_days="$2" && shift 2 ;; - --reviewed-list) need_value "$@" && reviewed="$2" && shift 2 ;; - *) die "unknown argument: $1" ;; + --context) need_value "$@" && context="$2" && shift 2 ;; + --cache-dir) need_value "$@" && cache_dir="$2" && shift 2 ;; + --from-dir) need_value "$@" && from_dir="$2" && shift 2 ;; + --snapshot-dir) need_value "$@" && snapshot_dir="$2" && shift 2 ;; + --now) need_value "$@" && now="$2" && shift 2 ;; + --vuln-max-age-days) need_value "$@" && vuln_max_age_days="$2" && shift 2 ;; + --reviewed-list) need_value "$@" && reviewed="$2" && shift 2 ;; + *) die "unknown argument: $1" ;; esac done diff --git a/scripts/cleanup-orphan-generated-vpas.sh b/scripts/cleanup-orphan-generated-vpas.sh index 8a8acf35ef..32fc0269ae 100755 --- a/scripts/cleanup-orphan-generated-vpas.sh +++ b/scripts/cleanup-orphan-generated-vpas.sh @@ -6,16 +6,22 @@ set -euo pipefail umask 077 # Stop on a proven safety refusal before attempting any further deletion. -fail() { printf 'REFUSED: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'REFUSED: %s\n' "$1" >&2 + exit 1 +} # Report incomplete evidence distinctly from a proven absence or safe result. -unknown() { printf 'UNKNOWN: %s\n' "$1" >&2; exit 2; } +unknown() { + printf 'UNKNOWN: %s\n' "$1" >&2 + exit 2 +} mode="${1:-assess}" -[[ "$#" -le 1 && ( "$mode" == assess || "$mode" == apply ) ]] || fail 'invalid operation' +[[ "$#" -le 1 && ("$mode" == assess || "$mode" == apply) ]] || fail 'invalid operation' [[ "${GITHUB_REPOSITORY:-}" == devantler-tech/platform && - "${GITHUB_EVENT_NAME:-}" == workflow_dispatch && - "${GITHUB_REF:-}" == refs/heads/main && - "${GITHUB_RUN_ATTEMPT:-}" == 1 && - "${VPA_CLEANUP_CONFIRM:-}" == cleanup-orphan-generated-vpas ]] || + "${GITHUB_EVENT_NAME:-}" == workflow_dispatch && + "${GITHUB_REF:-}" == refs/heads/main && + "${GITHUB_RUN_ATTEMPT:-}" == 1 && + "${VPA_CLEANUP_CONFIRM:-}" == cleanup-orphan-generated-vpas ]] || fail 'requires the confirmed first-attempt main workflow' root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" work="$(mktemp -d)" @@ -78,7 +84,10 @@ target_absent() { return 1 } -generated=0; retained=0; candidates=0; deleted=0 +generated=0 +retained=0 +candidates=0 +deleted=0 : >"$work/plan.jsonl" while IFS= read -r obj; do [[ -n "$obj" ]] || continue @@ -107,7 +116,8 @@ while IFS= read -r obj; do # overridden. The cleanup is solely for the old ownerless generation. if ! jq -e '(.metadata.ownerReferences // [] | length)==0 and (.metadata.finalizers // [] | length)==0 and .metadata.deletionTimestamp==null' <<<"$obj" >/dev/null; then - retained=$((retained + 1)); continue + retained=$((retained + 1)) + continue fi if target_absent "$obj"; then printf '%s\n' "$obj" >>"$work/plan.jsonl" diff --git a/scripts/diagnose-email-startup/probe.sh b/scripts/diagnose-email-startup/probe.sh index 49c9039431..99a337e698 100755 --- a/scripts/diagnose-email-startup/probe.sh +++ b/scripts/diagnose-email-startup/probe.sh @@ -10,7 +10,10 @@ flag() { if "$@" >/dev/null 2>&1; then printf true; else printf false; fi; } # while measuring, so a truncated record cannot masquerade as a shorter read. bounded_rows() { local task_rows - task_rows="$(head -c 1048577 "$1" || exit 3; printf '.')" || exit 3 + task_rows="$( + head -c 1048577 "$1" || exit 3 + printf '.' + )" || exit 3 task_rows="${task_rows%.}" [[ "${#task_rows}" -le 1048576 ]] || exit 3 printf '%s' "$task_rows" diff --git a/scripts/generate-publish-workflow-approved-revisions.sh b/scripts/generate-publish-workflow-approved-revisions.sh index 8e09a6fb39..8850d463aa 100755 --- a/scripts/generate-publish-workflow-approved-revisions.sh +++ b/scripts/generate-publish-workflow-approved-revisions.sh @@ -130,8 +130,14 @@ applied_revision() { } case "$count" in 1) ;; - 0) refuse "$repo: no OCIRepository on $context has url $url"; return 1 ;; - *) refuse "$repo: $count OCIRepositories on $context have url $url; the applied revision is ambiguous"; return 1 ;; + 0) + refuse "$repo: no OCIRepository on $context has url $url" + return 1 + ;; + *) + refuse "$repo: $count OCIRepositories on $context have url $url; the applied revision is ambiguous" + return 1 + ;; esac revision="$(printf '%s' "$json" | jq -r --arg u "$url" \ 'first(.items[] | select(.spec.url == $u)) | .status.artifact.revision // ""' 2>/dev/null)" || { @@ -141,7 +147,8 @@ applied_revision() { if is_digest "$revision"; then local binding file="${PUBLISH_BOOTSTRAP_FILE:-$REPO_ROOT/scripts/publish-workflow-bootstrap-publications.json}" binding="$(publication_binding "${PUBLISH_CONSUMER_ROOT:-$REPO_ROOT}" "$file" "$repo" publish-app "$revision")" || { - refuse "$repo: bare digest has no exact publication binding"; return 1; + refuse "$repo: bare digest has no exact publication binding" + return 1 } # artifact.revision is the registry manifest digest. artifact.digest hashes # Flux's local archive and must never be substituted for that identity. @@ -174,7 +181,7 @@ git_tag_for_registry_tag() { matches="$(printf '%s\n' "$tags" | while IFS= read -r t; do [ -n "$t" ] || continue [ "$(registry_tag_for_git_tag "$t")" = "$registry_tag" ] && printf '%s\n' "$t" - done || true)" # the loop's status is the LAST tag's comparison, never a verdict + done || true)" # the loop's status is the LAST tag's comparison, never a verdict count="$(printf '%s' "$matches" | grep -c . || true)" [ "$count" -eq 1 ] || return 1 plausible_ref "$matches" || return 1 @@ -339,7 +346,9 @@ default_release_resolver() { # OBS_COUNT. split_observation() { local answer="$1" field n=0 - OBS_TAG=''; OBS_DIGEST=''; OBS_SIGNER='' + OBS_TAG='' + OBS_DIGEST='' + OBS_SIGNER='' while IFS= read -r field; do case "$n" in 0) OBS_TAG="$field" ;; @@ -361,9 +370,15 @@ replace_generated_record() { case "$(uname -s)" in Linux) replacement_command='mv' ;; Darwin) replacement_command='gmv' ;; - *) refuse 'record replacement requires GNU coreutils'; return 1 ;; + *) + refuse 'record replacement requires GNU coreutils' + return 1 + ;; esac - command -v "$replacement_command" >/dev/null || { refuse 'record replacement requires GNU coreutils (gmv on macOS)'; return 1; } + command -v "$replacement_command" >/dev/null || { + refuse 'record replacement requires GNU coreutils (gmv on macOS)' + return 1 + } "$replacement_command" --no-target-directory -- "$1" "$2" } @@ -371,36 +386,53 @@ main() { local observer="${APPROVED_REVISION_OBSERVER:-}" pin_resolver="${APPROVED_REVISION_PIN_RESOLVER:-}" local release_resolver="${APPROVED_REVISION_RELEASE_RESOLVER:-}" release_candidates='' release_attempted=' ' local today="${APPROVED_REVISIONS_OBSERVED_ON:-$(date -u +%Y-%m-%d)}" - is_date "$today" || { refuse "APPROVED_REVISIONS_OBSERVED_ON must be YYYY-MM-DD, got '$today'"; exit 1; } + is_date "$today" || { + refuse "APPROVED_REVISIONS_OBSERVED_ON must be YYYY-MM-DD, got '$today'" + exit 1 + } [ "$OUTPUT" != "$IDENTITIES_OUTPUT" ] || { - refuse 'compatibility and observed identities outputs must be different files'; exit 1; + refuse 'compatibility and observed identities outputs must be different files' + exit 1 } if [ -L "$OUTPUT" ] || [ -L "$IDENTITIES_OUTPUT" ]; then - refuse 'generation outputs must not be symlinks'; exit 1 + refuse 'generation outputs must not be symlinks' + exit 1 fi local canonical_baseline_checked=0 local canonical_root="${PUBLISH_CONSUMER_ROOT:-$REPO_ROOT}" canonical_document canonical_file canonical_initial canonical_file="${CANONICAL_TENANT_BINDINGS_FILE:-$canonical_root/scripts/canonical-tenant-source-bindings.json}" - canonical_document="$(canonical_tenant_bindings "$canonical_root")" || { refuse 'canonical source admission is invalid'; exit 1; } + canonical_document="$(canonical_tenant_bindings "$canonical_root")" || { + refuse 'canonical source admission is invalid' + exit 1 + } canonical_initial="$canonical_document" local bootstrap_file="${PUBLISH_BOOTSTRAP_FILE:-$REPO_ROOT/scripts/publish-workflow-bootstrap-publications.json}" bootstrap_document='' if [ -e "$bootstrap_file" ] || [ -n "${PUBLISH_BOOTSTRAP_FILE:-}" ]; then bootstrap_document="$(read_bound_publications "${PUBLISH_CONSUMER_ROOT:-$REPO_ROOT}" "$bootstrap_file")" || { - refuse 'publication bindings are invalid; no file written'; exit 1; + refuse 'publication bindings are invalid; no file written' + exit 1 } fi if [ -f "$IDENTITIES_OUTPUT" ]; then [ "$(head -n1 "$IDENTITIES_OUTPUT")" = "$IDENTITIES_HEADER" ] || { - refuse 'observed identities header is malformed; no file written'; exit 1; + refuse 'observed identities header is malformed; no file written' + exit 1 } awk -F'\t' 'NR>1 && (NF!=7 || seen[$1]++) {bad=1} END {exit bad}' "$IDENTITIES_OUTPUT" || { - refuse 'observed identities rows are malformed or duplicated; no file written'; exit 1; + refuse 'observed identities rows are malformed or duplicated; no file written' + exit 1 } fi local consumers - consumers="$("$REPORT" --list-consumers)" || { refuse 'consumer discovery failed; see the report above'; exit 1; } - [ -n "$consumers" ] || { refuse 'consumer discovery returned nothing'; exit 1; } + consumers="$("$REPORT" --list-consumers)" || { + refuse 'consumer discovery failed; see the report above' + exit 1 + } + [ -n "$consumers" ] || { + refuse 'consumer discovery returned nothing' + exit 1 + } # One approved row per source repository and workflow: artifacts one repository publishes # share its revision set, so the artifact field is dropped and repeats collapse (#3327). consumers="$(printf '%s\n' "$consumers" | cut -f1-3 | awk '!seen[$0]++')" @@ -423,20 +455,36 @@ main() { # No legacy row/candidate is fabricated for a canonical-only consumer. # A failed observation or immutable publication read never becomes absence. local source_state source_inventory canonical_pin canonical_tuple canonical_previous canonical_date - canonical_observation_consistent "$canonical_record" "$IDENTITIES_OUTPUT" || { refuse "$repo: bootstrap/current-observation records disagree"; exit 1; } - canonical_publication_verify "$canonical_record" || { refuse "$repo: immutable publication verification failed"; exit 1; } + canonical_observation_consistent "$canonical_record" "$IDENTITIES_OUTPUT" || { + refuse "$repo: bootstrap/current-observation records disagree" + exit 1 + } + canonical_publication_verify "$canonical_record" || { + refuse "$repo: immutable publication verification failed" + exit 1 + } require_canonical_approval "$repo" "$workflow" "$(printf '%s\n' "$canonical_record" | jq -r .publication.publisherIdentity)" || exit 1 - [ -n "${PUBLISH_KUBE_CONTEXT:-}" ] || { refuse "$repo: source inventory context is unset"; exit 1; } + [ -n "${PUBLISH_KUBE_CONTEXT:-}" ] || { + refuse "$repo: source inventory context is unset" + exit 1 + } source_inventory="$(mktemp)" || exit 1 if ! kubectl --context "$PUBLISH_KUBE_CONTEXT" get ocirepositories.source.toolkit.fluxcd.io -A -o json >"$source_inventory"; then - rm -f "$source_inventory"; refuse "$repo: source inventory read failed"; exit 1 + rm -f "$source_inventory" + refuse "$repo: source inventory read failed" + exit 1 fi source_state="$(canonical_source_inventory_state "$canonical_record" "$source_inventory")" || { - rm -f "$source_inventory"; refuse "$repo: source inventory is incomplete, ambiguous or inconsistent"; exit 1; + rm -f "$source_inventory" + refuse "$repo: source inventory is incomplete, ambiguous or inconsistent" + exit 1 } rm -f "$source_inventory" canonical_pin="$(default_pin_resolver "$repo" "$workflow")" || exit 1 - [ "$canonical_pin" = "$(printf '%s\n' "$canonical_record" | jq -r .publication.publisherIdentity)" ] || { refuse "$repo: current publisher is outside the pinned admission"; exit 1; } + [ "$canonical_pin" = "$(printf '%s\n' "$canonical_record" | jq -r .publication.publisherIdentity)" ] || { + refuse "$repo: current publisher is outside the pinned admission" + exit 1 + } case "$source_state" in previous) # Keep the last actual row, including absence before first observation. @@ -444,7 +492,10 @@ main() { if [ -n "$canonical_previous" ]; then identities_rows="${identities_rows}${canonical_previous}"$'\n'; fi ;; absent | pending) - [ "$(printf '%s\n' "$canonical_record" | jq -r .kind)" = published-bootstrap ] || { refuse "$repo: observed publication cannot regress to bootstrap"; exit 1; } + [ "$(printf '%s\n' "$canonical_record" | jq -r .kind)" = published-bootstrap ] || { + refuse "$repo: observed publication cannot regress to bootstrap" + exit 1 + } ;; observed) canonical_tuple="$(printf '%s\n' "$canonical_record" | jq -r '[.consumer,.workflow,(.publication.gitTag|ltrimstr("v")),.sourceIdentity.digest,.publication.publisherIdentity,.publication.publisherIdentity]|@tsv')" @@ -456,7 +507,10 @@ main() { identities_rows="${identities_rows}${canonical_tuple}"$'\t'"${canonical_date}"$'\n' canonical_document="$(printf '%s\n' "$canonical_document" | jq --arg consumer "$repo" '.bindings |= map(if .consumer==$consumer then .kind="published-binding"|del(.previousBinding,.previousObservation) else . end)')" ;; - *) refuse "$repo: unknown canonical source state"; exit 1 ;; + *) + refuse "$repo: unknown canonical source state" + exit 1 + ;; esac continue fi @@ -490,14 +544,16 @@ main() { case "$signer_identity" in devantler-tech/.github/*) if ! require_canonical_approval "$repo" "$workflow" "$signer_identity"; then - unresolved=$((unresolved + 1)); continue + unresolved=$((unresolved + 1)) + continue fi OBS_TAG="$(printf '%s\n' "$previous_legacy" | cut -f1)" OBS_DIGEST="$(printf '%s\n' "$previous_legacy" | cut -f2)" OBS_SIGNER="$(printf '%s\n' "$previous_legacy" | cut -f3)" if ! plausible_tag "$OBS_TAG" || ! is_digest "$OBS_DIGEST" || ! is_sha "$OBS_SIGNER"; then refuse "$repo: retained legacy publication evidence is missing or malformed" - unresolved=$((unresolved + 1)); continue + unresolved=$((unresolved + 1)) + continue fi ;; *) OBS_SIGNER="${signer_identity##*@}" ;; @@ -505,12 +561,14 @@ main() { case "$pin_identity" in devantler-tech/.github/*) if ! require_canonical_approval "$repo" "$workflow" "$pin_identity"; then - unresolved=$((unresolved + 1)); continue + unresolved=$((unresolved + 1)) + continue fi pin="$(printf '%s\n' "$previous_legacy" | cut -f4)" if ! is_sha "$pin"; then refuse "$repo: retained legacy pin evidence is missing or malformed" - unresolved=$((unresolved + 1)); continue + unresolved=$((unresolved + 1)) + continue fi ;; *) pin="${pin_identity##*@}" ;; @@ -520,7 +578,11 @@ main() { local candidate='' case "$workflow" in publish-app | publish-manifests) ;; - *) refuse "$repo: unsupported publishing workflow '$workflow'"; unresolved=$((unresolved + 1)); continue ;; + *) + refuse "$repo: unsupported publishing workflow '$workflow'" + unresolved=$((unresolved + 1)) + continue + ;; esac case "$release_attempted" in *" $workflow "*) ;; @@ -564,7 +626,8 @@ main() { if ! printf '%s\n' "$bootstrap_document" | jq -e --arg repo "$repo" --arg digest "$actual_digest" --arg signer "$OBS_SIGNER" \ 'all(.publications[] | select(.consumer==$repo); .digest==$digest and .signerSHA==$signer)' >/dev/null; then refuse "$repo: observation differs from the immutable publication binding" - unresolved=$((unresolved + 1)); continue + unresolved=$((unresolved + 1)) + continue fi # Real observation retires the first-publication approval. Retain only # the immutable identity binding needed to attribute a bare Flux digest. @@ -576,7 +639,10 @@ main() { refuse "$unresolved of $examined consumer(s) unresolved; no file written (an incomplete approved set reads like a complete one)" exit 1 fi - [ "$examined" -gt 0 ] || { refuse 'no consumers examined; no file written'; exit 1; } + [ "$examined" -gt 0 ] || { + refuse 'no consumers examined; no file written' + exit 1 + } local tmp identities_tmp bootstrap_tmp='' canonical_tmp='' tmp="$(mktemp "${OUTPUT}.XXXXXX")" @@ -584,20 +650,23 @@ main() { trap 'rm -f "$tmp" "$identities_tmp" "${bootstrap_tmp:-}" "${canonical_tmp:-}"' EXIT if [ "$(printf '%s\n' "$canonical_document" | jq '.bindings|length')" -gt 0 ]; then if [ ! -f "$canonical_file" ] || [ -L "$canonical_file" ]; then - refuse 'canonical destination changed; no file written'; exit 1 + refuse 'canonical destination changed; no file written' + exit 1 fi # The reader normalizes JSON for validation; that formatting is not an # observation. Keep the original bytes unless the validated data changed. local canonical_unchanged canonical_unchanged="$(printf '%s\n' "$canonical_document" | jq -cr --argjson initial "$canonical_initial" '. == $initial')" || { - refuse 'canonical comparison failed; no file written'; exit 1; + refuse 'canonical comparison failed; no file written' + exit 1 } if [ "$canonical_unchanged" = false ]; then canonical_tmp="$(mktemp "${canonical_file}.XXXXXX")" printf '%s\n' "$canonical_document" | jq . >"$canonical_tmp" chmod 0644 "$canonical_tmp" elif [ "$canonical_unchanged" != true ]; then - refuse 'canonical comparison was not boolean; no file written'; exit 1 + refuse 'canonical comparison was not boolean; no file written' + exit 1 fi fi if [ -n "$bootstrap_document" ]; then diff --git a/scripts/guard-baseline-context-optin-scan-scope.sh b/scripts/guard-baseline-context-optin-scan-scope.sh index 66ec9ca421..4cadcbbbe0 100755 --- a/scripts/guard-baseline-context-optin-scan-scope.sh +++ b/scripts/guard-baseline-context-optin-scan-scope.sh @@ -135,7 +135,8 @@ declares_caveat() { # die "could not locate the '$LABEL' label line for '$ns' in '$file' — unparseable YAML, or the expression no longer matches" case "$label_line" in '' | *[!0-9]*) - die "yq returned no usable line number for the '$ns' opt-in label in '$file' (got '$label_line'); refusing to fall back to a file-wide marker search" ;; + die "yq returned no usable line number for the '$ns' opt-in label in '$file' (got '$label_line'); refusing to fall back to a file-wide marker search" + ;; esac [ "$label_line" -gt 0 ] || die "yq returned line 0 for the '$ns' opt-in label in '$file'; refusing to fall back to a file-wide marker search" diff --git a/scripts/guard-consumer-discovery-conservation.sh b/scripts/guard-consumer-discovery-conservation.sh index a4355edfcf..fcd4ab0314 100755 --- a/scripts/guard-consumer-discovery-conservation.sh +++ b/scripts/guard-consumer-discovery-conservation.sh @@ -350,7 +350,8 @@ validate_published_inputs() { fi case "$reference" in /* | *:* | git@* | *'?'* | *'#'*) - refuse "$label has an absolute or nonlocal reference outside the inspectable published k8s dependency closure, so its consumers are UNKNOWN" ;; + refuse "$label has an absolute or nonlocal reference outside the inspectable published k8s dependency closure, so its consumers are UNKNOWN" + ;; esac resolved="$(published_path "$dir/$reference" "$label dependency")" || exit 1 case "$mode" in @@ -359,7 +360,8 @@ validate_published_inputs() { [ -f "$resolved" ] || refuse "could not render $label, so its non-file published k8s dependency closure is UNKNOWN" # Builtin configs retain their Kustomization loader root, even when their # own config file lives in a subdirectory. - if [ "$mode" = loader ]; then validate_published_inputs "$resolved" "$dir" "$label loader"; fi ;; + if [ "$mode" = loader ]; then validate_published_inputs "$resolved" "$dir" "$label loader"; fi + ;; *) refuse "could not read the complete published k8s dependency closure of $label, so its consumers are UNKNOWN" ;; esac done <<<"$references" diff --git a/scripts/guard-coroot-baseline-context.sh b/scripts/guard-coroot-baseline-context.sh index 7a4975a603..ebba6a113a 100755 --- a/scripts/guard-coroot-baseline-context.sh +++ b/scripts/guard-coroot-baseline-context.sh @@ -25,16 +25,31 @@ manifest="${root_dir}/k8s/bases/infrastructure/controllers/coroot/namespace.yaml tolerated='[]' while [[ $# -gt 0 ]]; do case "$1" in - --context) context="${2:?context required}"; shift 2 ;; - --namespace-manifest) manifest="${2:?namespace manifest required}"; shift 2 ;; - --tolerate-unready) tolerated="${2:?tolerated record required}"; shift 2 ;; - *) printf 'Unknown argument: %s\n' "$1" >&2; exit 2 ;; + --context) + context="${2:?context required}" + shift 2 + ;; + --namespace-manifest) + manifest="${2:?namespace manifest required}" + shift 2 + ;; + --tolerate-unready) + tolerated="${2:?tolerated record required}" + shift 2 + ;; + *) + printf 'Unknown argument: %s\n' "$1" >&2 + exit 2 + ;; esac done -case "${phase}" in before-publish|after-reconcile) ;; *) exit 2 ;; esac +case "${phase}" in before-publish | after-reconcile) ;; *) exit 2 ;; esac scratch="$(mktemp -d)" trap 'rm -rf "${scratch}"' EXIT -fail() { printf '::error::Coroot baseline context: %s\n' "$1" >&2; exit 1; } +fail() { + printf '::error::Coroot baseline context: %s\n' "$1" >&2 + exit 1 +} output() { printf '%s=%s\n' "$1" "$2" if [[ -n "${GITHUB_OUTPUT:-}" ]]; then printf '%s=%s\n' "$1" "$2" >>"${GITHUB_OUTPUT}"; fi @@ -144,7 +159,10 @@ if [[ "${phase}" == before-publish ]]; then fi case "${configured}" in enabled) ;; - '') output rollout_required false; exit 0 ;; + '') + output rollout_required false + exit 0 + ;; *) fail "unexpected baseline-context-coroot value '${configured}'" ;; esac read_templates diff --git a/scripts/guard-ghcr-fanout-component-gate.sh b/scripts/guard-ghcr-fanout-component-gate.sh index adffdd5102..3a44f02a2e 100755 --- a/scripts/guard-ghcr-fanout-component-gate.sh +++ b/scripts/guard-ghcr-fanout-component-gate.sh @@ -54,7 +54,10 @@ readonly -a NON_APP_FANOUT_NAMESPACES=("kyverno") repo_root="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" -die() { printf '%s\n' "$1" >&2; exit 2; } +die() { + printf '%s\n' "$1" >&2 + exit 2 +} apps_dir="$repo_root/k8s/bases/apps" apps_kustomization="$apps_dir/kustomization.yaml" @@ -86,7 +89,10 @@ reachable_manifests() { # reachable_visited="${reachable_visited:-} $dir" kustomization="" for target in kustomization.yaml kustomization.yml Kustomization; do - [ -f "$dir/$target" ] && { kustomization="$dir/$target"; break; } + [ -f "$dir/$target" ] && { + kustomization="$dir/$target" + break + } done [ -n "$kustomization" ] || die "guard: $dir has no kustomization, so what it deploys is unknown" @@ -268,7 +274,10 @@ done # is an orphan — most often a typo in an app name. while IFS= read -r ns; do [ -n "$ns" ] || continue - is_non_app "$ns" && { printf ' ok %s — declared non-app fan-out namespace\n' "$ns"; continue; } + is_non_app "$ns" && { + printf ' ok %s — declared non-app fan-out namespace\n' "$ns" + continue + } listed_is_app=0 for app in "${ghcr_apps[@]}"; do [ "$app" = "$ns" ] && listed_is_app=1 && break; done ((listed_is_app)) && continue diff --git a/scripts/guard-helm-post-renderers.sh b/scripts/guard-helm-post-renderers.sh index 910113bc49..bf8266ba98 100755 --- a/scripts/guard-helm-post-renderers.sh +++ b/scripts/guard-helm-post-renderers.sh @@ -600,16 +600,16 @@ check_release() { # yq 'select(tag == "!!map")' "$wire_input" >"$work/$mode.manifests.yaml" || die "$label: cannot collect the combined $mode render of $chart_desc" fi - error="$(apply_post_renderers "$record" "$work/$mode.manifests.yaml" "$work/$mode.manifests")" - case $? in - 0) ;; - 1) - printf '%s: its post-renderers do not apply to the %s render of %s (%s): %s\n' \ - "$label" "$mode" "$chart_desc" "$hook_strategy" "$error" >>"$scratch/findings" - return 0 - ;; - *) exit 2 ;; - esac + error="$(apply_post_renderers "$record" "$work/$mode.manifests.yaml" "$work/$mode.manifests")" + case $? in + 0) ;; + 1) + printf '%s: its post-renderers do not apply to the %s render of %s (%s): %s\n' \ + "$label" "$mode" "$chart_desc" "$hook_strategy" "$error" >>"$scratch/findings" + return 0 + ;; + *) exit 2 ;; + esac done printf ' ok %s: %s post-renderer(s) apply to %s (install and upgrade)\n' \ "$label" "$(jq '.release.spec.postRenderers | length' "$record")" "$chart_desc" diff --git a/scripts/guard-longhorn-ui-baseline-context.sh b/scripts/guard-longhorn-ui-baseline-context.sh index 1a38b2984e..3a4fba74af 100755 --- a/scripts/guard-longhorn-ui-baseline-context.sh +++ b/scripts/guard-longhorn-ui-baseline-context.sh @@ -10,15 +10,27 @@ context=admin@prod release="${root_dir}/k8s/providers/hetzner/infrastructure/controllers/longhorn/helm-release.yaml" while [[ $# -gt 0 ]]; do case "$1" in - --context) context="${2:?context required}"; shift 2 ;; - --release) release="${2:?release file required}"; shift 2 ;; - *) printf 'Unknown argument: %s\n' "$1" >&2; exit 2 ;; + --context) + context="${2:?context required}" + shift 2 + ;; + --release) + release="${2:?release file required}" + shift 2 + ;; + *) + printf 'Unknown argument: %s\n' "$1" >&2 + exit 2 + ;; esac done -case "${phase}" in before-publish|after-reconcile) ;; *) exit 2 ;; esac +case "${phase}" in before-publish | after-reconcile) ;; *) exit 2 ;; esac scratch="$(mktemp -d)" trap 'rm -rf "${scratch}"' EXIT -fail() { printf '::error::Longhorn UI baseline canary: %s\n' "$1" >&2; exit 1; } +fail() { + printf '::error::Longhorn UI baseline canary: %s\n' "$1" >&2 + exit 1 +} output() { printf 'rollout_required=%s\n' "$1" if [[ -n "${GITHUB_OUTPUT:-}" ]]; then printf 'rollout_required=%s\n' "$1" >>"${GITHUB_OUTPUT}"; fi diff --git a/scripts/guard-oci-repository-verify.sh b/scripts/guard-oci-repository-verify.sh index 2c888eb0be..0c3d8a4391 100755 --- a/scripts/guard-oci-repository-verify.sh +++ b/scripts/guard-oci-repository-verify.sh @@ -279,9 +279,14 @@ while IFS= read -r root; do fi while IFS=$'\t' read -r url name has_verify provider ids_type ids_count incomplete issuer subject ref_type ref_key_count ref_semver; do [ -n "$url" ] || continue - url="$(decode "$url")"; name="$(decode "$name")"; provider="$(decode "$provider")" - ids_type="$(decode "$ids_type")"; issuer="$(decode "$issuer")"; subject="$(decode "$subject")" - ref_type="$(decode "$ref_type")"; ref_semver="$(decode "$ref_semver")" + url="$(decode "$url")" + name="$(decode "$name")" + provider="$(decode "$provider")" + ids_type="$(decode "$ids_type")" + issuer="$(decode "$issuer")" + subject="$(decode "$subject")" + ref_type="$(decode "$ref_type")" + ref_semver="$(decode "$ref_semver")" [ -n "$url$name" ] || continue url="$(normalise_url "$url")" # The second pattern is the literal characters `${` — a Flux substitution marker, @@ -306,7 +311,9 @@ while IFS= read -r root; do fail "$label: invalid Tenant source constructor" continue fi - ref_type='!!map'; ref_key_count=1; ref_semver="$REQUIRED_TENANT_STREAM_SEMVER" + ref_type='!!map' + ref_key_count=1 + ref_semver="$REQUIRED_TENANT_STREAM_SEMVER" fi case "$url" in "$WEDDING_STREAM_URL" | "$ASCOACHING_STREAM_URL" | "$TEMPLATED_TENANT_STREAM_URL") diff --git a/scripts/guard-publish-workflow-approved-revisions.sh b/scripts/guard-publish-workflow-approved-revisions.sh index 0e4efaa258..2bf69e0753 100755 --- a/scripts/guard-publish-workflow-approved-revisions.sh +++ b/scripts/guard-publish-workflow-approved-revisions.sh @@ -92,4 +92,4 @@ while IFS= read -r canonical_entry; do "$canonical_consumer" "$(printf '%s\n' "$canonical_entry" | jq -r .kind)" done < <(printf '%s\n' "$canonical_only" | jq -c '.bindings[]') printf 'guard-publish-workflow-approved-revisions: %d consumer matcher(s) agree with %s (enforce=%s)\n' \ - "$((${#EXPECTED_CONSUMERS[@]}+canonical_count))" "${APPROVED_SET#"$REPO_ROOT"/}" "$ENFORCE" + "$((${#EXPECTED_CONSUMERS[@]} + canonical_count))" "${APPROVED_SET#"$REPO_ROOT"/}" "$ENFORCE" diff --git a/scripts/guard-shared-publish-workflow-pin.sh b/scripts/guard-shared-publish-workflow-pin.sh index d938ff6b63..039404e53f 100755 --- a/scripts/guard-shared-publish-workflow-pin.sh +++ b/scripts/guard-shared-publish-workflow-pin.sh @@ -345,9 +345,9 @@ judge_family_subject() { return 1 ;; esac - canonical_part="${inner#*"$boundary"}" # \.yaml@ + canonical_part="${inner#*"$boundary"}" # \.yaml@ legacy_part="${inner%"$boundary$canonical_part"}" - legacy_part="${legacy_part#"$LEGACY_FAMILY"}" # \.yaml@ + legacy_part="${legacy_part#"$LEGACY_FAMILY"}" # \.yaml@ case "$legacy_part$canonical_part" in *"$LEGACY_FAMILY"* | *"$CANONICAL_FAMILY"*) printf '%s: two-family subject names a publisher family more than once (subject: %s)\n' \ diff --git a/scripts/guard-test-script-exec-bit.sh b/scripts/guard-test-script-exec-bit.sh index a84a452a60..4ab767c4ca 100755 --- a/scripts/guard-test-script-exec-bit.sh +++ b/scripts/guard-test-script-exec-bit.sh @@ -6,8 +6,9 @@ set -euo pipefail repo_root="${1:-.}" inventory="$(mktemp)" trap 'rm -f "$inventory"' EXIT -git -C "$repo_root" ls-files --stage -z -- 'scripts/tests/*.sh' > "$inventory" || { - echo '::error::cannot read tracked test entries' >&2; exit 2 +git -C "$repo_root" ls-files --stage -z -- 'scripts/tests/*.sh' >"$inventory" || { + echo '::error::cannot read tracked test entries' >&2 + exit 2 } count=0 status=0 @@ -21,7 +22,7 @@ while IFS= read -r -d '' record; do echo "::error::$path must be tracked 100755 at stage zero" >&2 status=1 fi -done < "$inventory" +done <"$inventory" if [[ "$count" == 0 ]]; then echo '::error::found no tracked root test entries; cannot verify execute bits' >&2 exit 2 diff --git a/scripts/judge-openbao-health/README.md b/scripts/judge-openbao-health/README.md index 40b5b3b63b..66be4fb4c7 100644 --- a/scripts/judge-openbao-health/README.md +++ b/scripts/judge-openbao-health/README.md @@ -17,7 +17,7 @@ go run ./scripts/judge-openbao-health \ ``` | Exit | Verdict | Meaning | -|---|---|---| +| --- | --- | --- | | 0 | `PROVEN` | Every expected server is healthy and the cluster has exactly one active server. | | 1 | `HOLD` | A definite finding: a sealed or uninitialized server, the wrong release, split cluster identities, or no active server or more than one. | | 2 | `UNKNOWN` | The evidence cannot decide: a server is missing, duplicated or unexpected, the capture is stale, from the future or has no start or completion time, a body was not produced during the capture, or a body is malformed, partial, oversized or ambiguous. Invalid arguments also exit 2, including `-h`, an unknown flag and any flag given twice. | diff --git a/scripts/judge-openbao-quorum/README.md b/scripts/judge-openbao-quorum/README.md index d5f88cde14..d89cc5c193 100644 --- a/scripts/judge-openbao-quorum/README.md +++ b/scripts/judge-openbao-quorum/README.md @@ -19,7 +19,7 @@ go run ./scripts/judge-openbao-quorum \ ``` | Exit | Verdict | Meaning | -|---|---|---| +| --- | --- | --- | | 0 | `PROVEN` | Every expected server is a voter, exactly one member is leader, nothing unexpected is a member, and quorum survives taking `--replace` out. | | 1 | `HOLD` | A definite finding: an expected server that is missing or not a voter, an unexpected member, no leader or more than one, or a replacement that would leave fewer voters than quorum. | | 2 | `UNKNOWN` | The evidence cannot decide: the capture is stale, from the future or has no start or completion time, the read did not return HTTP 200, or the body is malformed, partial, oversized or ambiguous, or a member is listed twice or without its node identity, voter or leader field. Invalid arguments also exit 2, including `-h`, an unknown flag, any flag given twice and a `--replace` that is not one of `--expect-servers`. | diff --git a/scripts/prepare-email-credentials.sh b/scripts/prepare-email-credentials.sh index e394170916..178a32ecec 100755 --- a/scripts/prepare-email-credentials.sh +++ b/scripts/prepare-email-credentials.sh @@ -8,7 +8,7 @@ fail() { } mode=$EMAIL_CUSTODY_MODE -case "$mode" in plan|copy) ;; *) fail ;; esac +case "$mode" in plan | copy) ;; *) fail ;; esac [ -n "$BAO_ADDR" ] || fail [ -f "$EMAIL_CUSTODY_TOKEN_FILE" ] || fail [ -d "$EMAIL_CUSTODY_WORK_DIR" ] || fail @@ -16,8 +16,8 @@ work=$(mktemp -d "$EMAIL_CUSTODY_WORK_DIR/email-credentials.XXXXXXXX") || fail trap 'rm -rf "$work"' 0 trap 'exit 1' HUP INT TERM token=$(cat "$EMAIL_CUSTODY_TOKEN_FILE") || fail -case "$token" in ''|*[!A-Za-z0-9._-]*) fail ;; esac -printf 'X-Vault-Token: %s\nContent-Type: application/json\n' "$token" > "$work/headers" +case "$token" in '' | *[!A-Za-z0-9._-]*) fail ;; esac +printf 'X-Vault-Token: %s\nContent-Type: application/json\n' "$token" >"$work/headers" unset token # Headers and JSON bodies are files in private memory, never process arguments. @@ -35,13 +35,13 @@ request() { POST) set -- "$@" --request POST --data-binary "@$request_input" ;; *) fail ;; esac - status=$(curl "$@" "$BAO_ADDR/v1/$request_path" 2> "$work/request-error") || fail + status=$(curl "$@" "$BAO_ADDR/v1/$request_path" 2>"$work/request-error") || fail } read_record() { record_path=$1 record_output=$2 - : > "$record_output" + : >"$record_output" request GET "secret/data/$record_path" '' case "$status" in 404) return 4 ;; @@ -55,7 +55,7 @@ read_record() { select(all(..; type != "number")) | select(.password | type == "string") | select(.password | test("^[A-Za-z0-9]{40,128}$"))' \ - "$work/response.json" > "$record_output" 2> "$work/json-error"; then + "$work/response.json" >"$record_output" 2>"$work/json-error"; then fail fi } @@ -79,16 +79,16 @@ report() { # without creating passwords or mistaking absence for a verified migration. request GET sys/mounts '' [ "$status" = 200 ] || fail -if ! jq -e '.data | type == "object"' "$work/response.json" > /dev/null 2> "$work/json-error"; then +if ! jq -e '.data | type == "object"' "$work/response.json" >/dev/null 2>"$work/json-error"; then fail fi -if jq -e '.data["secret/"] == null' "$work/response.json" > /dev/null; then +if jq -e '.data["secret/"] == null' "$work/response.json" >/dev/null; then unseeded=3 report exit 0 fi if ! jq -e '.data["secret/"] | .type == "kv" and .options.version == "2"' \ - "$work/response.json" > /dev/null 2> "$work/json-error"; then + "$work/response.json" >/dev/null 2>"$work/json-error"; then fail fi @@ -98,18 +98,18 @@ fi if [ "$mode" = copy ]; then expected_file=${EMAIL_CUSTODY_EXPECTED_FILE:-} [ -n "$expected_file" ] && [ -f "$expected_file" ] && [ ! -L "$expected_file" ] || fail - [ "$(wc -c < "$expected_file")" -le 1048576 ] || fail + [ "$(wc -c <"$expected_file")" -le 1048576 ] || fail if ! jq -ceS -s 'select(length == 1) | .[0] | select(type == "object") | select(keys == ["apps/private-mail/admin", "apps/private-mail/agent", "apps/private-mail/maintainer"]) | select(all(.[]; type == "object" and all(..; type != "number") and (.password | type == "string" and test("^[A-Za-z0-9]{40,128}$"))))' \ - "$expected_file" > "$work/expected.json" 2> "$work/json-error"; then + "$expected_file" >"$work/expected.json" 2>"$work/json-error"; then fail fi for account in admin maintainer agent; do if ! jq -ceS --arg path "apps/private-mail/$account" '.[$path]' \ - "$work/expected.json" > "$work/expected-$account.json" 2> "$work/json-error"; then + "$work/expected.json" >"$work/expected-$account.json" 2>"$work/json-error"; then fail fi read_record "apps/private-mail/$account" "$work/source.json" || fail @@ -150,12 +150,12 @@ for account in admin maintainer agent; do pending=$((pending + 1)) else if ! jq -cn --slurpfile value "$work/source.json" \ - '{options: {cas: 0}, data: $value[0]}' > "$work/create.json" 2> "$work/json-error"; then + '{options: {cas: 0}, data: $value[0]}' >"$work/create.json" 2>"$work/json-error"; then fail fi request POST "secret/data/$destination_path" "$work/create.json" creation_status=$status - case "$creation_status" in 200|204|400) ;; *) fail ;; esac + case "$creation_status" in 200 | 204 | 400) ;; *) fail ;; esac # Read back successful writes AND conditional-create races. Never replace # a different destination or silently accept a lost/uncertain write. read_record "$destination_path" "$work/destination.json" || fail diff --git a/scripts/private-mail-config/inventory-request.json b/scripts/private-mail-config/inventory-request.json index 2fdcc99415..c586783f0f 100644 --- a/scripts/private-mail-config/inventory-request.json +++ b/scripts/private-mail-config/inventory-request.json @@ -1 +1,110 @@ -{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:NetworkListener/query",{"limit":16,"calculateTotal":true},"listeners-query"],["x:NetworkListener/get",{"#ids":{"resultOf":"listeners-query","name":"x:NetworkListener/query","path":"/ids"},"properties":["id","name","protocol","bind","useTls","tlsImplicit","overrideProxyTrustedNetworks","tlsDisableProtocols"]},"listeners-get"],["x:MtaHook/query",{"limit":1,"calculateTotal":true},"MtaHook"],["x:MtaMilter/query",{"limit":1,"calculateTotal":true},"MtaMilter"],["x:WebHook/query",{"limit":1,"calculateTotal":true},"WebHook"],["x:Domain/query",{"limit":16,"calculateTotal":true},"domains-query"],["x:Domain/get",{"#ids":{"resultOf":"domains-query","name":"x:Domain/query","path":"/ids"},"properties":["id","name","aliases","directoryId"]},"domains-get"],["x:Application/query",{"limit":2,"calculateTotal":true},"applications-query"],["x:Application/get",{"#ids":{"resultOf":"applications-query","name":"x:Application/query","path":"/ids"},"properties":["id","description","enabled","resourceUrl","urlPrefix","unpackDirectory","oauthClientId"]},"applications-get"],["x:Account/query",{"limit":16,"calculateTotal":true},"accounts-query"],["x:Account/get",{"#ids":{"resultOf":"accounts-query","name":"x:Account/query","path":"/ids"},"properties":["id","@type","name","domainId","aliases"]},"accounts-get"],["x:MailingList/query",{"limit":16,"calculateTotal":true},"mailing-lists-query"],["x:MailingList/get",{"#ids":{"resultOf":"mailing-lists-query","name":"x:MailingList/query","path":"/ids"},"properties":["id","name","domainId","aliases"]},"mailing-lists-get"]]} +{ + "using": ["urn:ietf:params:jmap:core", "urn:stalwart:jmap"], + "methodCalls": [ + [ + "x:NetworkListener/query", + { "limit": 16, "calculateTotal": true }, + "listeners-query" + ], + [ + "x:NetworkListener/get", + { + "#ids": { + "resultOf": "listeners-query", + "name": "x:NetworkListener/query", + "path": "/ids" + }, + "properties": [ + "id", + "name", + "protocol", + "bind", + "useTls", + "tlsImplicit", + "overrideProxyTrustedNetworks", + "tlsDisableProtocols" + ] + }, + "listeners-get" + ], + ["x:MtaHook/query", { "limit": 1, "calculateTotal": true }, "MtaHook"], + ["x:MtaMilter/query", { "limit": 1, "calculateTotal": true }, "MtaMilter"], + ["x:WebHook/query", { "limit": 1, "calculateTotal": true }, "WebHook"], + [ + "x:Domain/query", + { "limit": 16, "calculateTotal": true }, + "domains-query" + ], + [ + "x:Domain/get", + { + "#ids": { + "resultOf": "domains-query", + "name": "x:Domain/query", + "path": "/ids" + }, + "properties": ["id", "name", "aliases", "directoryId"] + }, + "domains-get" + ], + [ + "x:Application/query", + { "limit": 2, "calculateTotal": true }, + "applications-query" + ], + [ + "x:Application/get", + { + "#ids": { + "resultOf": "applications-query", + "name": "x:Application/query", + "path": "/ids" + }, + "properties": [ + "id", + "description", + "enabled", + "resourceUrl", + "urlPrefix", + "unpackDirectory", + "oauthClientId" + ] + }, + "applications-get" + ], + [ + "x:Account/query", + { "limit": 16, "calculateTotal": true }, + "accounts-query" + ], + [ + "x:Account/get", + { + "#ids": { + "resultOf": "accounts-query", + "name": "x:Account/query", + "path": "/ids" + }, + "properties": ["id", "@type", "name", "domainId", "aliases"] + }, + "accounts-get" + ], + [ + "x:MailingList/query", + { "limit": 16, "calculateTotal": true }, + "mailing-lists-query" + ], + [ + "x:MailingList/get", + { + "#ids": { + "resultOf": "mailing-lists-query", + "name": "x:MailingList/query", + "path": "/ids" + }, + "properties": ["id", "name", "domainId", "aliases"] + }, + "mailing-lists-get" + ] + ] +} diff --git a/scripts/private-mail-config/stage-webui.sh b/scripts/private-mail-config/stage-webui.sh index 1a895b8414..910f0a923a 100755 --- a/scripts/private-mail-config/stage-webui.sh +++ b/scripts/private-mail-config/stage-webui.sh @@ -1,9 +1,16 @@ #!/usr/bin/env bash set -euo pipefail umask 077 -[ "$#" = 1 ] && [ -d "$1" ] && [ ! -L "$1" ] || { echo 'An existing application build directory is required.' >&2; exit 2; } +[ "$#" = 1 ] && [ -d "$1" ] && [ ! -L "$1" ] || { + echo 'An existing application build directory is required.' >&2 + exit 2 +} destination=$1 -case "$destination" in /*) ;; *) echo 'Application build directory must be absolute.' >&2; exit 2 ;; esac +case "$destination" in /*) ;; *) + echo 'Application build directory must be absolute.' >&2 + exit 2 + ;; +esac for name in webui.zip webui.LICENSE webui.SOURCE; do if [ -e "$destination/$name" ] || [ -L "$destination/$name" ]; then echo 'Administration build artifacts already exist; refusing to replace them.' >&2 @@ -16,7 +23,8 @@ url="$(jq -er '.url | select(type == "string")' "$root/webui-pins.json")" digest="$(jq -er '.sha256 | select(type == "string" and test("^[0-9a-f]{64}$"))' "$root/webui-pins.json")" license_digest="$(jq -er '.license_sha256 | select(type == "string" and test("^[0-9a-f]{64}$"))' "$root/webui-pins.json")" [ "$url" = "https://github.com/stalwartlabs/webui/releases/download/v${version}/webui.zip" ] || { - echo 'Application release metadata is invalid.' >&2; exit 2; + echo 'Application release metadata is invalid.' >&2 + exit 2 } work="$(mktemp -d "$destination/.webui-stage.XXXXXX")" trap 'rm -rf "$work"' EXIT @@ -30,7 +38,10 @@ if ! printf '%s %s\n%s %s\n' "$digest" "$work/webui.zip" "$license_digest" "$w fi printf 'Unmodified upstream source: https://github.com/stalwartlabs/webui/tree/v%s\n' "$version" >"$work/webui.SOURCE" for name in webui.zip webui.LICENSE webui.SOURCE; do - (set -C; cat "$work/$name" >"$destination/$name") + ( + set -C + cat "$work/$name" >"$destination/$name" + ) chmod 0644 "$destination/$name" done echo 'Verified pinned administration assets staged.' diff --git a/scripts/prove-cilium-default-deny.sh b/scripts/prove-cilium-default-deny.sh index ef8cb75a0d..90d174a145 100755 --- a/scripts/prove-cilium-default-deny.sh +++ b/scripts/prove-cilium-default-deny.sh @@ -2,7 +2,10 @@ # Explicitly dispatched disposable Docker proof. Never uses a production config. set -euo pipefail umask 077 -fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} [[ "${GITHUB_ACTIONS:-}" == true && "${GITHUB_REPOSITORY:-}" == devantler-tech/platform && "${GITHUB_REF:-}" == refs/heads/main ]] || fail 'main-only Actions runner required' [[ "${CONFIRM_DISPOSABLE_CILIUM_PROOF:-}" == RUN_DISPOSABLE_CILIUM_PROOF ]] || fail 'explicit disposable-cluster confirmation required' [[ "${GITHUB_RUN_ID:-}" =~ ^[0-9]+$ && "${GITHUB_RUN_ATTEMPT:-}" =~ ^[0-9]+$ && "${GITHUB_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || fail 'invalid run identity' @@ -21,15 +24,18 @@ receipt_update() { } owned_containers() { docker ps -aq --filter "label=io.x-k8s.kind.cluster=${name}"; } cleanup() { - if [[ ! -f "${work}/owner.json" ]]; then [[ ! -d "$work" ]]; return; fi + if [[ ! -f "${work}/owner.json" ]]; then + [[ ! -d "$work" ]] + return + fi jq -e --arg name "$name" --arg head "$GITHUB_SHA" --arg config "$kubeconfig" \ '.name == $name and .workflow_head == $head and .kubeconfig == $config and .provider == "Docker"' "${work}/owner.json" >/dev/null || return 1 # Recheck the config, not only its receipt, before allowing a deletion. Both # calls are explicit: ambient context/provider can never choose the target. [[ "$(yq '.metadata.name' "${work}/ksail.yaml")" == "$name" && - "$(yq '.spec.cluster.provider' "${work}/ksail.yaml")" == Docker && - "$(yq '.spec.cluster.distribution' "${work}/ksail.yaml")" == Vanilla && - "$(yq '.spec.cluster.connection.kubeconfig' "${work}/ksail.yaml")" == "$kubeconfig" ]] || return 1 + "$(yq '.spec.cluster.provider' "${work}/ksail.yaml")" == Docker && + "$(yq '.spec.cluster.distribution' "${work}/ksail.yaml")" == Vanilla && + "$(yq '.spec.cluster.connection.kubeconfig' "${work}/ksail.yaml")" == "$kubeconfig" ]] || return 1 local containers containers="$(owned_containers)" || return 1 if [[ -n "$containers" ]]; then @@ -51,7 +57,10 @@ finish() { exit "$result" } case "${1:-}" in - cleanup) cleanup; exit ;; + cleanup) + cleanup + exit + ;; run) ;; *) fail 'usage: prove-cilium-default-deny.sh run|cleanup' ;; esac @@ -64,9 +73,9 @@ ksail_version="$(ksail --version)" || fail 'KSail version read failed' grep -Eq '^ksail version v?7[.]193[.]8([[:space:]]|$)' <<<"$ksail_version" || fail 'audited KSail 7.193.8 required' jq -e '.schema == 1 and .repository == "devantler-tech/platform" and (.head | test("^[0-9a-f]{40}$")) and (.pr > 0)' "${candidate}/candidate.json" >/dev/null || fail 'candidate receipt absent or invalid' [[ "$(sha256sum "${candidate}/generator.yaml" | cut -d' ' -f1)" == "$(jq -r '.generator_sha256' "${candidate}/candidate.json")" && - "$(sha256sum "${candidate}/flux-copy.yaml" | cut -d' ' -f1)" == "$(jq -r '.flux_copy_sha256' "${candidate}/candidate.json")" ]] || fail 'candidate files differ from verified hashes' +"$(sha256sum "${candidate}/flux-copy.yaml" | cut -d' ' -f1)" == "$(jq -r '.flux_copy_sha256' "${candidate}/candidate.json")" ]] || fail 'candidate files differ from verified hashes' [[ "$(yq '.kind + "/" + .metadata.name' "${candidate}/generator.yaml")" == ClusterPolicy/add-default-deny && - "$(yq '.kind + "/" + .metadata.name' "${candidate}/flux-copy.yaml")" == CiliumNetworkPolicy/default-deny ]] || fail 'unexpected fixed candidate resources' +"$(yq '.kind + "/" + .metadata.name' "${candidate}/flux-copy.yaml")" == CiliumNetworkPolicy/default-deny ]] || fail 'unexpected fixed candidate resources' mkdir -p "${work}/receipt" jq --arg workflow "$GITHUB_SHA" --arg run "$GITHUB_RUN_ID" --arg attempt "$GITHUB_RUN_ATTEMPT" \ '. + {workflow_head:$workflow,run_id:$run,run_attempt:$attempt,ksail_version:"7.193.8",cilium_version:"1.20.2",verdict:"UNKNOWN",cleanup:"UNKNOWN",floor_tests:{},tests:{},floor_valid_cnp_count:0,valid_cnp_count:0}' \ @@ -122,7 +131,7 @@ shopt -s nullglob generated=("${work}"/generated/*.yaml) shopt -u nullglob [[ "${#generated[@]}" -gt 0 ]] || fail 'Kyverno generated no resources' -yq -o=json -I=0 'select(.metadata.namespace == "deny-proof" and .kind == "CiliumNetworkPolicy" and (.metadata.name == "default-deny" or .metadata.name == "allow-dns"))' "${generated[@]}" | \ +yq -o=json -I=0 'select(.metadata.namespace == "deny-proof" and .kind == "CiliumNetworkPolicy" and (.metadata.name == "default-deny" or .metadata.name == "allow-dns"))' "${generated[@]}" | jq -s '{apiVersion:"v1",kind:"List",items:.}' >"${work}/runtime-policies.yaml" jq -e '[.items[].metadata.name] | sort == ["allow-dns","default-deny"]' "${work}/runtime-policies.yaml" >/dev/null || fail 'exact generated CNP/DNS floor missing' generated_rules="$(jq -cS '.items[] | select(.kind == "CiliumNetworkPolicy" and .metadata.name == "default-deny") | {spec,specs}' "${work}/runtime-policies.yaml")" @@ -180,8 +189,16 @@ http() { # shellcheck disable=SC2016 k -n deny-proof exec "$1" -- sh -c 'wget -T 3 -q -O - "$1" 2>&1; result=$?; printf "\nPROBE_EXIT=%s\n" "$result"' probe "http://$2:8080/" } -admitted() { local result; result="$(http "$1" "$2")" || return 1; [[ "$result" == *deny-proof-ok* && "${result##*$'\n'}" == PROBE_EXIT=0 ]]; } -denied() { local result; result="$(http "$1" "$2")" || return 1; [[ "$result" == *'timed out'* && "${result##*$'\n'}" == PROBE_EXIT=1 ]]; } +admitted() { + local result + result="$(http "$1" "$2")" || return 1 + [[ "$result" == *deny-proof-ok* && "${result##*$'\n'}" == PROBE_EXIT=0 ]] +} +denied() { + local result + result="$(http "$1" "$2")" || return 1 + [[ "$result" == *'timed out'* && "${result##*$'\n'}" == PROBE_EXIT=1 ]] +} dns() { k -n deny-proof exec client-allowed -- nslookup kubernetes.default.svc.cluster.local >/dev/null; } http_routes() { admitted client-allowed "$allowed_ip" || return 1 @@ -196,7 +213,7 @@ http_routes() { # and admitted HTTP alongside the expected result for each negative pair. traffic_proof() { local mode="$1" require_dns="$2" consecutive=0 attempt - for ((attempt=0; attempt<12; attempt++)); do + for ((attempt = 0; attempt < 12; attempt++)); do probes_unchanged || fail 'probe readiness or UID/IP/runtime identity changed during traffic proof' if admitted server-denied 127.0.0.1 && { [[ "$require_dns" == false ]] || dns; } && http_routes "$mode" && admitted server-denied 127.0.0.1; then consecutive=$((consecutive + 1)) diff --git a/scripts/prove-kyverno-stale-report-prune.sh b/scripts/prove-kyverno-stale-report-prune.sh index 2941d3f83a..f1f64faad1 100755 --- a/scripts/prove-kyverno-stale-report-prune.sh +++ b/scripts/prove-kyverno-stale-report-prune.sh @@ -131,7 +131,7 @@ never_team_mid="$(result_timestamp never-rescanned require-team-label team-label owner_before="$(result_timestamp excluded-later require-owner-label owner-label)" wait_for "one more complete background scan" 600 scan_ran [[ "$(result_timestamp never-rescanned require-owner-label owner-label)" == "$never_owner_mid" && - "$(result_timestamp never-rescanned require-team-label team-label)" == "$never_team_mid" ]] || +"$(result_timestamp never-rescanned require-team-label team-label)" == "$never_team_mid" ]] || fail "a completed scan rewrote a never-rescanned result, so that report is still being scanned" has_results never-rescanned "$both" || fail "never-rescanned lost its results without the policy" log "ok: defect reproduced, the excluded rule's result survives a completed scan unchanged" diff --git a/scripts/publish-workflow-approved-revisions.lib.sh b/scripts/publish-workflow-approved-revisions.lib.sh index 21911228cb..a1edefad75 100644 --- a/scripts/publish-workflow-approved-revisions.lib.sh +++ b/scripts/publish-workflow-approved-revisions.lib.sh @@ -127,7 +127,10 @@ ref_matches_approved() { local ref="$1" body got want shift case "$ref" in - '('*')') body="${ref#\(}"; body="${body%\)}" ;; + '('*')') + body="${ref#\(}" + body="${body%\)}" + ;; *'|'*) return 1 ;; *) body="$ref" ;; esac @@ -148,7 +151,7 @@ while IFS= read -r row; do tabs="${row//[!$'\t']/}" IFS=$'\t' read -r consumer workflow applied_tag applied_digest signer pin candidate observed_on extra <<<"$row" [ -n "$consumer" ] || continue - [ "$line_no" -gt 2 ] || [ "$consumer" != 'consumer' ] || continue # the header + [ "$line_no" -gt 2 ] || [ "$consumer" != 'consumer' ] || continue # the header { [ "${#tabs}" -le 7 ] && [ -z "${extra:-}" ]; } || refuse "approved set line $line_no has more than eight fields" { [ "${#tabs}" -eq 7 ] && [ -n "$observed_on" ]; } || refuse "approved set line $line_no has fewer than eight fields" plausible_repo "$consumer" || refuse "approved set line $line_no names an implausible consumer '$consumer'" @@ -237,7 +240,7 @@ canonical="" line_no=0 while IFS= read -r row || [ -n "$row" ]; do line_no=$((line_no + 1)) - [ "$line_no" -gt 1 ] || continue # the header, checked above + [ "$line_no" -gt 1 ] || continue # the header, checked above tabs="${row//[!$'\t']/}" [ "${#tabs}" -eq 3 ] || refuse "canonical approvals line $line_no does not have exactly four fields" IFS=$'\t' read -r consumer workflow repository commit <<<"$row" @@ -274,7 +277,7 @@ while IFS= read -r canonical_entry; do canonical_pin="${canonical_publisher##*@}" canonical_approved="$(lookup "$canonical" "$canonical_consumer")" case "$canonical_approved" in - "$canonical_pin"|"($canonical_pin|"*")"|"("*"|$canonical_pin)") ;; + "$canonical_pin" | "($canonical_pin|"*")" | "("*"|$canonical_pin)") ;; *) refuse "$canonical_consumer: canonical-only publisher is not an exact approved member" ;; esac done < <(printf '%s\n' "$canonical_only" | jq -c '.bindings[]') @@ -288,8 +291,10 @@ canonical_ref_ok() { '('*'|'*')') ;; *) return 1 ;; esac - first="${1#\(}"; first="${first%%\|*}" - second="${1#*\|}"; second="${second%\)}" + first="${1#\(}" + first="${first%%\|*}" + second="${1#*\|}" + second="${second%\)}" [ "$1" = "($first|$second)" ] && is_sha "$first" && is_sha "$second" && [ "$first" != "$second" ] } @@ -325,7 +330,7 @@ while IFS= read -r file; do [ -n "$file" ] || continue rel="${file#"$SCAN_ROOT"/}" case "$rel" in - scripts/*) continue ;; # test fixtures under scripts/ carry subjects that verify nothing + scripts/*) continue ;; # test fixtures under scripts/ carry subjects that verify nothing esac is_generic=0 for generic in "${GENERIC_SUBJECT_FILES[@]}"; do @@ -363,7 +368,8 @@ while IFS= read -r file; do # OCIRepository selection, and would otherwise pass unjudged. line_count="$(grep -cE "$FAMILY_SUBJECT_PATTERN" "$file" || true)" [ "$line_count" = "1" ] || refuse "$rel: $line_count shared-publish-workflow subject lines found but exactly one OCIRepository entry was read; a subject outside the OCIRepository document is unjudged" - repo="${url#oci://ghcr.io/devantler-tech/}"; repo="${repo%%/*}" + repo="${url#oci://ghcr.io/devantler-tech/}" + repo="${repo%%/*}" repo="$(oci_name_to_repo "$repo")" plausible_repo "$repo" || refuse "$rel: consumer name '$repo' derived from $url is implausible" [ -n "$(lookup "$approved" "$repo")" ] || refuse "$rel is a shared-workflow consumer ($repo) with no row in the approved set and no entry in GENERIC_SUBJECT_FILES" @@ -373,7 +379,7 @@ while IFS= read -r file; do canonical_ref='-' case "$subject" in "$SUBJECT_PREFIX"*) - rest="${subject#"$SUBJECT_PREFIX"}" # \.yaml@$ + rest="${subject#"$SUBJECT_PREFIX"}" # \.yaml@$ case "$rest" in *'$') rest="${rest%\$}" ;; *) refuse "$rel: subject is not anchored with a trailing \$: $subject" ;; @@ -395,7 +401,7 @@ while IFS= read -r file; do "$LEGACY_FAMILY"*"$family_boundary"*) ;; *) refuse "$rel: two-family subject is not the legacy family followed by the canonical family: $subject" ;; esac - canonical_part="${inner#*"$family_boundary"}" # \.yaml@ + canonical_part="${inner#*"$family_boundary"}" # \.yaml@ legacy_part="${inner%"$family_boundary$canonical_part"}" case "$canonical_part" in *"$CANONICAL_FAMILY"* | *"$LEGACY_FAMILY"*) refuse "$rel: two-family subject names a publisher family more than once: $subject" ;; @@ -406,7 +412,7 @@ while IFS= read -r file; do esac canonical_ref="${canonical_part#*\\.yaml@}" canonical_ref_ok "$canonical_ref" || refuse "$rel: canonical family pins '@$canonical_ref', which is not one 40-hex commit, or two distinct ones written as (first|second)" - rest="${legacy_part#"$LEGACY_FAMILY"}" # \.yaml@ + rest="${legacy_part#"$LEGACY_FAMILY"}" # \.yaml@ [ "publish-${canonical_part%%\\.yaml@*}" = "publish-${rest%%\\.yaml@*}" ] || refuse "$rel: the two publisher families name different workflows: $subject" ;; diff --git a/scripts/read-actual-budget-userns-maps.sh b/scripts/read-actual-budget-userns-maps.sh index bc00c87a2d..ae4fdfe21a 100755 --- a/scripts/read-actual-budget-userns-maps.sh +++ b/scripts/read-actual-budget-userns-maps.sh @@ -163,8 +163,8 @@ classify_map() { fi # Linux permits any ordering, but neither side of the map may overlap. for ((index = 0; index < lines; index++)); do - if [[ ( "${inside}" -lt "${inside_ends[index]}" && "${inside_starts[index]}" -lt "${inside_end}" ) || - ( "${outside}" -lt "${outside_ends[index]}" && "${outside_starts[index]}" -lt "${outside_end}" ) ]]; then + if [[ ("${inside}" -lt "${inside_ends[index]}" && "${inside_starts[index]}" -lt "${inside_end}") || + ("${outside}" -lt "${outside_ends[index]}" && "${outside_starts[index]}" -lt "${outside_end}") ]]; then return 1 fi done diff --git a/scripts/read-cilium-api-limiter.sh b/scripts/read-cilium-api-limiter.sh index ec4668405e..55ec51f399 100755 --- a/scripts/read-cilium-api-limiter.sh +++ b/scripts/read-cilium-api-limiter.sh @@ -14,12 +14,18 @@ if [[ "$#" -ne 2 || "$1" != --context || "$2" != admin@prod ]]; then exit 1 fi for tool in kubectl jq timeout head; do - command -v "${tool}" >/dev/null 2>&1 || { printf 'Required diagnostic tool is unavailable.\n' >&2; exit 1; } + command -v "${tool}" >/dev/null 2>&1 || { + printf 'Required diagnostic tool is unavailable.\n' >&2 + exit 1 + } done umask 077 work_dir="$(mktemp -d)" trap 'rm -rf "${work_dir}"' EXIT -inconclusive() { printf 'INCONCLUSIVE: %s\n' "$1"; exit 3; } +inconclusive() { + printf 'INCONCLUSIVE: %s\n' "$1" + exit 3 +} # Bound JSON responses and requests. Neither failed kubectl output nor parser diagnostics are public. read_json() { @@ -73,7 +79,10 @@ read_json() { read_failure="${json_shape}" return 1 ;; - *) read_failure=INVALID_JSON; return 1 ;; + *) + read_failure=INVALID_JSON + return 1 + ;; esac } roster() { diff --git a/scripts/reassert-flux-root-verify.sh b/scripts/reassert-flux-root-verify.sh index 631edb0e3b..582c0a18d5 100755 --- a/scripts/reassert-flux-root-verify.sh +++ b/scripts/reassert-flux-root-verify.sh @@ -144,7 +144,10 @@ fi # instance_rendered -> true once flux-operator has reconciled the patched generation (or # nothing was patched), false while it has not. instance_rendered() { - [[ -z "${patched_generation}" ]] && { printf 'true'; return 0; } + [[ -z "${patched_generation}" ]] && { + printf 'true' + return 0 + } kubectl_prod get fluxinstance "${instance}" -o json | jq -r --argjson g "${patched_generation}" \ 'any(.status.conditions[]?; .type == "Ready" and .status == "True" and (.observedGeneration // -1) >= $g)' diff --git a/scripts/refresh-email-recovery-backup.sh b/scripts/refresh-email-recovery-backup.sh index 7f94f6a54e..0bcaf0d07b 100755 --- a/scripts/refresh-email-recovery-backup.sh +++ b/scripts/refresh-email-recovery-backup.sh @@ -6,7 +6,10 @@ stage=dispatch work='' watchers=() readonly owner_bashpid="$BASHPID" -fail() { printf 'EMAIL_BACKUP_REFRESH=UNVERIFIED stage=%s\n' "$stage" >&2; exit 2; } +fail() { + printf 'EMAIL_BACKUP_REFRESH=UNVERIFIED stage=%s\n' "$stage" >&2 + exit 2 +} cleanup() { # A terminated watch may inherit this EXIT handler before Bash resets it. # Only the parent owns the workspace and the watch processes. @@ -23,7 +26,7 @@ work=$(mktemp -d "$EMAIL_BACKUP_WORK_DIR/snapshot-refresh.XXXXXXXX") || fail kc=(kubectl --context admin@prod --namespace openbao --request-timeout=25s) job="vault-snapshot-refresh-${GITHUB_RUN_ID}-1" stage=source -"${kc[@]}" get cronjob vault-snapshot -o json > "$work/source.json" 2> "$work/reader.log" || fail +"${kc[@]}" get cronjob vault-snapshot -o json >"$work/source.json" 2>"$work/reader.log" || fail jq -e ' .kind=="CronJob" and .metadata.name=="vault-snapshot" and .metadata.namespace=="openbao" and (.metadata.uid|test("^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$")) and @@ -46,7 +49,7 @@ jq -e ' $p.containers[0].image=="quay.io/minio/aistor/mc:RELEASE.2026-03-12T04-18-55Z@sha256:6c33dc0fbf65c362be95003cd010ed95a41c556500833ea139f86de40c4c4e9f" and ([$p.containers[0].env[]|select(.name=="REQUIRE_REMOTE_SNAPSHOT")|.value]==["true"]) and ([$p.volumes[]|select(.persistentVolumeClaim)|{name,claim:.persistentVolumeClaim.claimName}]==[{name:"snapshots",claim:"vault-snapshots-recovery"}])) -' "$work/source.json" > /dev/null 2> "$work/admission.log" || fail +' "$work/source.json" >/dev/null 2>"$work/admission.log" || fail # Construct exactly the observed template and retain its CronJob controller owner. # Native history cleanup retains that ownership; a manual create still needs # its own unfinished-Job check because Forbid governs scheduled creation. @@ -57,11 +60,11 @@ jq --arg name "$job" ' annotations:((.spec.jobTemplate.metadata.annotations//{})+{"cronjob.kubernetes.io/instantiate":"manual"}), ownerReferences:[{apiVersion:"batch/v1",kind:"CronJob",name:$source.metadata.name,uid:$source.metadata.uid,controller:true}]}, spec:.spec.jobTemplate.spec} -' "$work/source.json" > "$work/job.json" 2> "$work/manifest.log" || fail -"${kc[@]}" get cronjob vault-snapshot -o json > "$work/rebound.json" 2> "$work/reader.log" || fail -jq -e --slurpfile before "$work/source.json" '.metadata.uid==$before[0].metadata.uid and .metadata.resourceVersion==$before[0].metadata.resourceVersion and .spec==$before[0].spec and ((.status.active//[])|length==0)' "$work/rebound.json" > /dev/null 2> "$work/admission.log" || fail +' "$work/source.json" >"$work/job.json" 2>"$work/manifest.log" || fail +"${kc[@]}" get cronjob vault-snapshot -o json >"$work/rebound.json" 2>"$work/reader.log" || fail +jq -e --slurpfile before "$work/source.json" '.metadata.uid==$before[0].metadata.uid and .metadata.resourceVersion==$before[0].metadata.resourceVersion and .spec==$before[0].spec and ((.status.active//[])|length==0)' "$work/rebound.json" >/dev/null 2>"$work/admission.log" || fail stage=overlap -"${kc[@]}" get jobs --chunk-size=0 -o json > "$work/jobs.json" 2> "$work/reader.log" || fail +"${kc[@]}" get jobs --chunk-size=0 -o json >"$work/jobs.json" 2>"$work/reader.log" || fail jq -e --slurpfile source "$work/source.json" ' (((.apiVersion=="v1" and .kind=="List") or (.apiVersion=="batch/v1" and .kind=="JobList")) and (.items|type)=="array" and (.metadata.continue//"")=="") and @@ -72,38 +75,38 @@ jq -e --slurpfile source "$work/source.json" ' then .metadata.deletionTimestamp==null and (.status.active//0)==0 and ([.status.conditions[]?|select((.type=="Complete" or .type=="Failed") and .status=="True")]|length)==1 else true end)) -' "$work/jobs.json" > /dev/null 2> "$work/admission.log" || fail +' "$work/jobs.json" >/dev/null 2>"$work/admission.log" || fail stage=create-unknown -"${kc[@]}" create -f "$work/job.json" -o json > "$work/created.json" 2> "$work/create.log" || fail +"${kc[@]}" create -f "$work/job.json" -o json >"$work/created.json" 2>"$work/create.log" || fail jq -e --arg name "$job" --slurpfile source "$work/source.json" ' .kind=="Job" and .metadata.name==$name and .metadata.namespace=="openbao" and (.metadata.uid|test("^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$")) and any(.metadata.ownerReferences[]; .kind=="CronJob" and .uid==$source[0].metadata.uid and .controller==true) -' "$work/created.json" > /dev/null 2> "$work/admission.log" || fail +' "$work/created.json" >/dev/null 2>"$work/admission.log" || fail stage=completion -"${kc[@]}" wait --for=condition=Complete "job/$job" --timeout=1850s > "$work/complete.log" 2>&1 & +"${kc[@]}" wait --for=condition=Complete "job/$job" --timeout=1850s >"$work/complete.log" 2>&1 & watchers+=("$!") -"${kc[@]}" wait --for=condition=Failed "job/$job" --timeout=1850s > "$work/failed.log" 2>&1 & +"${kc[@]}" wait --for=condition=Failed "job/$job" --timeout=1850s >"$work/failed.log" 2>&1 & watchers+=("$!") # A native watch only wakes the readback; it is never itself a success verdict. -wait -n "${watchers[@]}" 2> "$work/wait.log" || true +wait -n "${watchers[@]}" 2>"$work/wait.log" || true for pid in "${watchers[@]}"; do kill "$pid" 2>/dev/null || true; done for pid in "${watchers[@]}"; do wait "$pid" 2>/dev/null || true; done watchers=() -"${kc[@]}" get job "$job" -o json > "$work/completed.json" 2> "$work/reader.log" || fail +"${kc[@]}" get job "$job" -o json >"$work/completed.json" 2>"$work/reader.log" || fail jq -e --slurpfile created "$work/created.json" ' .metadata.uid==$created[0].metadata.uid and .metadata.name==$created[0].metadata.name and .metadata.namespace=="openbao" and .metadata.ownerReferences==$created[0].metadata.ownerReferences and .status.succeeded==1 and any(.status.conditions[]; .type=="Complete" and .status=="True") and all(.status.conditions[]; .type!="Failed" or .status!="True") -' "$work/completed.json" > /dev/null 2> "$work/admission.log" || fail +' "$work/completed.json" >/dev/null 2>"$work/admission.log" || fail stage=pod-proof uid=$(jq -er '.metadata.uid' "$work/created.json") || fail -"${kc[@]}" get pods -l "batch.kubernetes.io/controller-uid=$uid" -o json > "$work/pods.json" 2> "$work/reader.log" || fail +"${kc[@]}" get pods -l "batch.kubernetes.io/controller-uid=$uid" -o json >"$work/pods.json" 2>"$work/reader.log" || fail jq -e --arg uid "$uid" ' [.items[]|select(any(.metadata.ownerReferences[]?;.kind=="Job" and .uid==$uid and .controller==true))|select(.status.phase=="Succeeded")] | length==1 and all(.[]; ([.status.initContainerStatuses[]|select(.state.terminated.exitCode==0)|.name]|sort)==["install-mirror-tools","snapshot"] and ([.status.containerStatuses[]|select(.state.terminated.exitCode==0)|.name]==["mirror"])) -' "$work/pods.json" > /dev/null 2> "$work/admission.log" || fail +' "$work/pods.json" >/dev/null 2>"$work/admission.log" || fail printf '%s\n' 'EMAIL_BACKUP_REFRESH=COMPLETED snapshot=1 mirror=1 restore=UNVERIFIED' diff --git a/scripts/remove-wedding-shared-backup-pod.sh b/scripts/remove-wedding-shared-backup-pod.sh index 350dbe8fe3..4df46a540e 100644 --- a/scripts/remove-wedding-shared-backup-pod.sh +++ b/scripts/remove-wedding-shared-backup-pod.sh @@ -27,7 +27,7 @@ fi while IFS= read -r key; do printf '%s\n' "${key}" | grep -Eq '^[A-Za-z0-9][A-Za-z0-9._-]*/(base|wals)/[A-Za-z0-9._/-]+$' || fail 'an object key is outside the catalogue layout' case "/${key}/" in - *'//'*|*'/../'*|*'/./'*) fail 'an object key contains an unsafe path component' ;; + *'//'* | *'/../'* | *'/./'*) fail 'an object key contains an unsafe path component' ;; esac grep -qF "\"key\":\"${key}\"" "${work}/listing" || fail 'an object key was not in the current coverage listing' printf 'store/%s/%s/%s\n' "${BUCKET}" "${PREFIX}" "${key}" >>"${work}/removal-targets" diff --git a/scripts/report-publish-workflow-signing-revisions.sh b/scripts/report-publish-workflow-signing-revisions.sh index 0f9d6ea170..a5cab38485 100755 --- a/scripts/report-publish-workflow-signing-revisions.sh +++ b/scripts/report-publish-workflow-signing-revisions.sh @@ -229,7 +229,10 @@ discover_consumers() { if [ -n "$typed_documents" ]; then typed_file="$(mktemp)" || return 1 printf '%s\n' "$typed_documents" >"$typed_file" - rows="$(consumer_rows "$typed_file" "$mode")" || { rm -f "$typed_file"; return 1; } + rows="$(consumer_rows "$typed_file" "$mode")" || { + rm -f "$typed_file" + return 1 + } rm -f "$typed_file" found="$found$rows"$'\n' fi @@ -321,8 +324,8 @@ consumer_rows() { fi if [ "$mode" = with-object-contract ]; then if [ "$literal_identity" != true ] || [ -z "$verification" ] || [ -n "$extra" ] || - { [ "$filtered" != true ] && [ "$filtered" != false ]; } || - { [ "$active" != true ] && [ "$active" != false ]; }; then + { [ "$filtered" != true ] && [ "$filtered" != false ]; } || + { [ "$active" != true ] && [ "$active" != false ]; }; then printf 'OCI object contract in %s is incomplete or has no literal identity, so its consumers are UNKNOWN\n' "$file" >&2 return 1 fi diff --git a/scripts/required-merge-workflows.sh b/scripts/required-merge-workflows.sh index e97ad4c4ed..ec357a5134 100755 --- a/scripts/required-merge-workflows.sh +++ b/scripts/required-merge-workflows.sh @@ -2,7 +2,10 @@ # Read the effective rules for main, including inherited organization rules. # This is a one-shot read. It neither updates a PR nor enqueues one. set -euo pipefail -unknown() { printf 'Required merge workflows: UNKNOWN — %s\n' "$*" >&2; exit 2; } +unknown() { + printf 'Required merge workflows: UNKNOWN — %s\n' "$*" >&2 + exit 2 +} pages="$(gh api repos/devantler-tech/platform/rules/branches/main --paginate --slurp)" || unknown 'effective branch rules could not be read' result="$(jq -ce ' @@ -27,5 +30,5 @@ result="$(jq -ce ' | {path,repository_id,ref,ruleset_id:$rule.ruleset_id, ruleset_source_type:$rule.ruleset_source_type,ruleset_source:$rule.ruleset_source}] | unique | sort_by(.path,.repository_id,.ref,.ruleset_id) -' <<< "$pages")" || unknown 'effective workflow rules could not be interpreted' +' <<<"$pages")" || unknown 'effective workflow rules could not be interpreted' printf '%s\n' "$result" diff --git a/scripts/retire-arc-pool.sh b/scripts/retire-arc-pool.sh index cf3c80e153..596254a4f1 100755 --- a/scripts/retire-arc-pool.sh +++ b/scripts/retire-arc-pool.sh @@ -11,31 +11,43 @@ GITHUB_RUN_ID=${GITHUB_RUN_ID:-} GITHUB_RUN_ATTEMPT=${GITHUB_RUN_ATTEMPT:-} GITHUB_SHA=${GITHUB_SHA:-} [[ "$GITHUB_ACTIONS" == true && "$GITHUB_REPOSITORY" == devantler-tech/platform && - "$GITHUB_RUN_ID" =~ ^[1-9][0-9]{0,19}$ && "$GITHUB_RUN_ATTEMPT" =~ ^[1-9][0-9]{0,4}$ && - "$GITHUB_SHA" =~ ^[0-9a-f]{40}$ && - ( "$GITHUB_REF" == refs/heads/main || "$GITHUB_REF" == refs/heads/gh-readonly-queue/main/* ) ]] || - { printf 'ARC retirement requires the protected production workflow.\n' >&2; exit 1; } -[[ "$#" == 1 && ( "$1" == before-publish || "$1" == after-reconcile ) ]] || exit 2 + "$GITHUB_RUN_ID" =~ ^[1-9][0-9]{0,19}$ && "$GITHUB_RUN_ATTEMPT" =~ ^[1-9][0-9]{0,4}$ && + "$GITHUB_SHA" =~ ^[0-9a-f]{40}$ && + ("$GITHUB_REF" == refs/heads/main || "$GITHUB_REF" == refs/heads/gh-readonly-queue/main/*) ]] || + { + printf 'ARC retirement requires the protected production workflow.\n' >&2 + exit 1 + } +[[ "$#" == 1 && ("$1" == before-publish || "$1" == after-reconcile) ]] || exit 2 mode=$1 repo_root=$(cd "$(dirname "$0")/.." && pwd) cd "$repo_root" controller=k8s/bases/infrastructure/controllers/actions-runner-controller/helm-release.yaml [[ "$(yq -r '.metadata.annotations."platform.devantler.tech/arc-recovery" // ""' "$controller")" == drain-only ]] || - { printf 'ARC retirement is not armed by this declaration.\n'; exit; } + { + printf 'ARC retirement is not armed by this declaration.\n' + exit + } [[ "$(yq -r '.spec.suspend' "$controller")" == false && - "$(yq -r '.spec.values.flags.watchSingleNamespace' "$controller")" == arc-runners ]] || exit 1 +"$(yq -r '.spec.values.flags.watchSingleNamespace' "$controller")" == arc-runners ]] || exit 1 work=$(mktemp -d) proxy_pid='' stage=initialization cleanup() { - if [[ -n "$proxy_pid" ]]; then kill "$proxy_pid" 2>/dev/null || true; wait "$proxy_pid" 2>/dev/null || true; fi + if [[ -n "$proxy_pid" ]]; then + kill "$proxy_pid" 2>/dev/null || true + wait "$proxy_pid" 2>/dev/null || true + fi rm -rf "$work" } trap cleanup EXIT trap 'exit 143' INT TERM trap 'printf "ARC retirement refused at %s.\n" "$stage" >&2' ERR kc() { kubectl --context admin@prod "$@" 2>"$work/kubectl-error"; } -fail() { printf 'ARC retirement refused at %s.\n' "$1" >&2; exit 1; } +fail() { + printf 'ARC retirement refused at %s.\n' "$1" >&2 + exit 1 +} stage=administrator-identity kc auth whoami -o json >"$work/identity.json" jq -e '.kind=="SelfSubjectReview" and (.status.userInfo.groups | index("system:masters")!=null)' "$work/identity.json" >/dev/null @@ -99,7 +111,7 @@ metadata_proxy() { >"$work/proxy.log" 2>"$work/proxy-error" & proxy_pid=$! local tries - for ((tries=0; tries<100; tries++)); do + for ((tries = 0; tries < 100; tries++)); do if [[ -s "$work/proxy.log" ]]; then break; fi kill -0 "$proxy_pid" 2>/dev/null || fail metadata-proxy-start sleep 0.1 @@ -130,14 +142,19 @@ secret_absent() { check_json "$work/secret-metadata.json" case "$code" in 404) jq -e '.kind=="Status" and .reason=="NotFound" and .code==404' "$work/secret-metadata.json" >/dev/null || fail secret-metadata-not-found ;; - 200) jq -e '.apiVersion=="meta.k8s.io/v1" and .kind=="PartialObjectMetadata" and + 200) + jq -e '.apiVersion=="meta.k8s.io/v1" and .kind=="PartialObjectMetadata" and .metadata.name=="arc-github-app" and .metadata.namespace=="arc-runners" and - (.metadata.uid|type=="string" and length>0)' "$work/secret-metadata.json" >/dev/null || fail secret-metadata-response; return 1 ;; + (.metadata.uid|type=="string" and length>0)' "$work/secret-metadata.json" >/dev/null || fail secret-metadata-response + return 1 + ;; *) fail secret-metadata-read ;; esac } absence_receipts() { - children=true; nodes=true; secret=true + children=true + nodes=true + secret=true local kind for kind in autoscalingrunnersets autoscalinglisteners ephemeralrunnersets ephemeralrunners; do if ! empty_metadata "/apis/actions.github.com/v1alpha1/namespaces/arc-runners/$kind"; then children=false; fi @@ -180,7 +197,10 @@ delete_owned() { refresh_objects bind_current case "$phase" in - uninstalling) drain_receipts || fail changed-drain-proof; controller_replaced || fail changed-controller-proof ;; + uninstalling) + drain_receipts || fail changed-drain-proof + controller_replaced || fail changed-controller-proof + ;; credential-removing) absence_receipts ;; *) fail unexpected-delete-phase ;; esac @@ -201,37 +221,45 @@ delete_owned() { preconditions:{uid:.metadata.uid,resourceVersion:.metadata.resourceVersion}}' "$file" >"$work/delete.json" kc delete --raw "$path" -f "$work/delete.json" >"$work/delete-response.json" } -drained=false; children=false; nodes=false; secret=false; source=false; replaced=false; claimed=false; writer_current=false +drained=false +children=false +nodes=false +secret=false +source=false +replaced=false +claimed=false +writer_current=false printf 'null\n' >"$work/controller.json" printf '[]\n' >"$work/controller-pods.json" printf 'null\n' >"$work/receipt.json" printf 'null\n' >"$work/source-proof.json" refresh_objects if jq -e '.==null' "$work/hr.json" >/dev/null && - jq -e '.!=null' "$work/ars.json" >/dev/null && - jq -e '(.metadata.annotations//{} | has("platform.devantler.tech/arc-retirement")) | not' "$work/ns.json" >/dev/null; then + jq -e '.!=null' "$work/ars.json" >/dev/null && + jq -e '(.metadata.annotations//{} | has("platform.devantler.tech/arc-retirement")) | not' "$work/ns.json" >/dev/null; then # No durable UID or original release remains to authorize deletion. Keep the # orphan intact and name the missing ownership proof before claiming anything. fail unbound-orphan-scale-set fi if jq -e '.==null' "$work/hr.json" >/dev/null && - jq -e '.==null' "$work/eso.json" >/dev/null && - jq -e '.metadata.annotations | has("platform.devantler.tech/arc-retirement") | not' "$work/ns.json" >/dev/null; then + jq -e '.==null' "$work/eso.json" >/dev/null && + jq -e '.metadata.annotations | has("platform.devantler.tech/arc-retirement") | not' "$work/ns.json" >/dev/null; then secret_absent || fail lingering-generated-credential printf 'ARC retirement has no installed release or credential-sync object; the unchanged absence guard follows.\n' exit fi remaining=${ARC_RETIREMENT_REMAINING:-1200} [[ "$remaining" =~ ^[0-9]{1,4}$ && "$remaining" -gt 0 && "$remaining" -le 1200 ]] || fail retirement-deadline -deadline=$((SECONDS+remaining)) +deadline=$((SECONDS + remaining)) nonce=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n') [[ "$nonce" =~ ^[0-9a-f]{32}$ ]] || fail request-nonce ticket="native-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$nonce" await() { - local stage_name=$1; shift + local stage_name=$1 + shift stage=$stage_name until "$@"; do - ((SECONDS/dev/null || return 0 plan bind if jq -e --slurpfile ns "$work/ns.json" '.[-1].value."platform.devantler.tech/arc-retirement" != $ns[0].metadata.annotations."platform.devantler.tech/arc-retirement"' "$work/patch.json" >/dev/null; then - apply_ns; inspect + apply_ns + inspect if [[ "$phase" == fenced ]]; then [[ "$mode" == before-publish ]] || fail late-baseline-after-publication local restarts=${ARC_RETIREMENT_RESTARTS:-0} [[ "$restarts" =~ ^[0-3]$ && "$restarts" -lt 3 ]] || fail late-baseline-restart-bound - export ARC_RETIREMENT_RESTARTS=$((restarts+1)) - export ARC_RETIREMENT_REMAINING=$((deadline-SECONDS)) + export ARC_RETIREMENT_RESTARTS=$((restarts + 1)) + export ARC_RETIREMENT_REMAINING=$((deadline - SECONDS)) cleanup exec bash scripts/retire-arc-pool.sh "$mode" fi @@ -322,7 +351,7 @@ admission_receipts() { {op:"add",path:"/spec/target/name",value:"arc-retirement-proof"}]' "$work/eso.json" >"$work/eso-update-probe.json" denied_patch externalsecret arc-github-app fence-credential-recreation "$work/eso-update-probe.json" fi - case "$phase" in uninstalled|credential-removing|absent|restored) + case "$phase" in uninstalled | credential-removing | absent | restored) jq --arg namespace arc-systems '{apiVersion:"v1",kind:"Pod",metadata:{name:"arc-retirement-listener-proof",namespace:$namespace, labels:{"actions.github.com/scale-set-name":"platform-linux","actions.github.com/scale-set-namespace":"arc-runners","app.kubernetes.io/component":"runner-scale-set-listener"}},spec:.spec.template.spec}' "$work/controller.json" >"$work/pod-probe.json" denied_create fence-retired-listener-pod "$work/pod-probe.json" @@ -330,7 +359,7 @@ admission_receipts() { denied_create fence-listener-retirement "$work/zero-listener-probe.json" ;; esac - case "$phase" in credential-removing|absent|restored) + case "$phase" in credential-removing | absent | restored) jq -n '{apiVersion:"v1",kind:"Secret",metadata:{name:"arc-github-app",namespace:"arc-runners"},type:"Opaque"}' >"$work/secret-probe.json" denied_create fence-retired-secret "$work/secret-probe.json" ;; @@ -354,7 +383,10 @@ drain_receipts() { --slurpfile listeners "$work/listener-items.json" --slurpfile pods "$work/listener-pods.json" \ --argjson idle "$idle" --argjson capacity "$capacity" \ '{HR:$hr[0],ARS:$ars[0],ERS:$ers[0],Listeners:$listeners[0],ListenerPods:$pods[0],ChildrenAbsent:$idle,NodesAbsent:$capacity}' >"$work/drain.json" || fail drain-projection - if "$work/planner" check-drain <"$work/drain.json" >"$work/proof-output" 2>"$work/proof-error"; then drained=true; return 0; fi + if "$work/planner" check-drain <"$work/drain.json" >"$work/proof-output" 2>"$work/proof-error"; then + drained=true + return 0 + fi return 1 } controller_replaced() { @@ -364,14 +396,17 @@ controller_replaced() { jq -n --slurpfile dep "$work/controller.json" --slurpfile pods "$work/controller-pods.json" \ --slurpfile sets "$work/replicasets.json" --slurpfile journal "$work/journal.json" \ '{Deployment:$dep[0],Pods:$pods[0],ReplicaSets:$sets[0].items,Journal:$journal[0],Ticket:$journal[0].controllerTicket}' >"$work/controller-proof.json" || fail controller-projection - if "$work/planner" check-controller <"$work/controller-proof.json" >"$work/proof-output" 2>"$work/proof-error"; then replaced=true; return 0; fi + if "$work/planner" check-controller <"$work/controller-proof.json" >"$work/proof-output" 2>"$work/proof-error"; then + replaced=true + return 0 + fi return 1 } no_pool() { refresh_objects bind_current if jq -e '.baseline!=null' "$work/journal.json" >/dev/null && - jq -e '.==null' "$work/hr.json" >/dev/null && jq -e '.!=null' "$work/ars.json" >/dev/null; then + jq -e '.==null' "$work/hr.json" >/dev/null && jq -e '.!=null' "$work/ars.json" >/dev/null; then # A chart CREATE accepted before uninstall can leave its scale set behind. # Only the journal-bound orphan is removed, after the unchanged drain and # controller retirement proofs; ARC finalizers retire children naturally. @@ -407,7 +442,10 @@ inactive_source() { kc -n flux-system get kustomizations flux-system infrastructure-controllers infrastructure -o json >"$work/layers.json" || fail source-layers-read jq -n --slurpfile src "$work/source.json" --slurpfile layers "$work/layers.json" --arg digest "${PLATFORM_MANIFEST_DIGEST:?published digest required}" --arg ticket "$ticket" \ '{OCI:$src[0],Kustomizations:$layers[0].items,Digest:$digest,Ticket:$ticket}' >"$work/source-proof.json" || fail source-projection - if "$work/planner" check-source <"$work/source-proof.json" >"$work/proof-output" 2>"$work/proof-error"; then source=true; return 0; fi + if "$work/planner" check-source <"$work/source-proof.json" >"$work/proof-output" 2>"$work/proof-error"; then + source=true + return 0 + fi return 1 } source_writer_quiesced() { @@ -441,12 +479,15 @@ writer_current_receipt() { kc -n flux-system get kustomizations flux-system infrastructure-controllers infrastructure -o json >"$work/writer-current-layers.json" || fail current-writer-layers-read jq -n --slurpfile src "$work/writer-current-source.json" --slurpfile layers "$work/writer-current-layers.json" \ --slurpfile journal "$work/journal.json" '{OCI:$src[0],Kustomizations:$layers[0].items,Writer:$journal[0].baseline.writer}' >"$work/writer-current-proof.json" || fail current-writer-projection - if jq -e '.baseline.writer.flux!=null' "$work/journal.json" >/dev/null;then + if jq -e '.baseline.writer.flux!=null' "$work/journal.json" >/dev/null; then flux_writer_receipts jq --slurpfile flux "$work/flux-writer.json" '.+{Flux:$flux[0]}' "$work/writer-current-proof.json" >"$work/writer-proof.json" || fail current-writer-runtime-projection mv "$work/writer-proof.json" "$work/writer-current-proof.json" fi - if "$work/planner" check-writer <"$work/writer-current-proof.json" >"$work/proof-output" 2>"$work/proof-error"; then writer_current=true;return 0;fi + if "$work/planner" check-writer <"$work/writer-current-proof.json" >"$work/proof-output" 2>"$work/proof-error"; then + writer_current=true + return 0 + fi return 1 } ensure_writer_barrier() { @@ -454,12 +495,14 @@ ensure_writer_barrier() { # Requiring layer Ready again would deadlock on the correctly closed CREATE # path. Reuse instead requires unchanged UID/configuration generations and a # currently verified signed OCI digest; changed authority gets a full barrier. - if jq -e '.baseline.writer!=null' "$work/journal.json" >/dev/null && writer_current_receipt;then return 0;fi + if jq -e '.baseline.writer!=null' "$work/journal.json" >/dev/null && writer_current_receipt; then return 0; fi flux_writer_receipts cp "$work/flux-writer.json" "$work/flux-writer-before.json" fresh_source_request await quiesced-source-writer source_writer_quiesced - plan writer;apply_ns;inspect + plan writer + apply_ns + inspect writer_current_receipt || fail writer-barrier-record } @@ -473,17 +516,20 @@ if jq -e '.metadata.annotations|has("platform.devantler.tech/arc-retirement")' " if jq -e --slurpfile hr "$work/hr.json" --slurpfile eso "$work/eso.json" --slurpfile ars "$work/ars.json" '.baseline!=null and ((.hrUID=="" and $hr[0]!=null) or (.esoUID=="" and $eso[0]!=null) or (.baseline.arsUID=="" and $ars[0]!=null))' "$work/journal.json" >/dev/null; then pending_binding=true; fi if [[ "$pending_binding" == false ]]; then - case "$phase" in drained|quiescing|quiesced) await current-drain drain_receipts ;; uninstalling) if jq -e '.!=null' "$work/hr.json" >/dev/null; then await current-drain drain_receipts; fi ;; esac - case "$phase" in quiesced|uninstalling) await current-controller controller_replaced ;; esac - case "$phase" in uninstalled|credential-removing|absent|restored) absence_receipts ;; esac + case "$phase" in drained | quiescing | quiesced) await current-drain drain_receipts ;; uninstalling) if jq -e '.!=null' "$work/hr.json" >/dev/null; then await current-drain drain_receipts; fi ;; esac + case "$phase" in quiesced | uninstalling) await current-controller controller_replaced ;; esac + case "$phase" in uninstalled | credential-removing | absent | restored) absence_receipts ;; esac fi - prior_run=$(jq -er '.owner.run' "$work/journal.json"); prior_attempt=$(jq -er '.owner.attempt' "$work/journal.json") + prior_run=$(jq -er '.owner.run' "$work/journal.json") + prior_attempt=$(jq -er '.owner.attempt' "$work/journal.json") if [[ "$prior_run" != "$GITHUB_RUN_ID" || "$prior_attempt" != "$GITHUB_RUN_ATTEMPT" ]]; then [[ "$prior_run" =~ ^[1-9][0-9]{0,19}$ && "$prior_attempt" =~ ^[1-9][0-9]{0,4}$ ]] || fail previous-owner-identity GH_TOKEN=${GH_TOKEN:?workflow token required} gh api "repos/devantler-tech/platform/actions/runs/$prior_run/attempts/$prior_attempt" >"$work/receipt.json" 2>"$work/github-error" || fail previous-attempt-read fi fi -plan claim; apply_ns; inspect +plan claim +apply_ns +inspect claimed=true bind_current admission_receipts @@ -492,8 +538,10 @@ if [[ "$mode" == after-reconcile ]]; then fresh_source_request await inactive-source inactive_source absence_receipts - refresh_objects; bind_current - plan restored; apply_ns + refresh_objects + bind_current + plan restored + apply_ns printf 'ARC retirement restored the exact inactive artifact with a durable closed fence.\n' exit fi @@ -514,14 +562,19 @@ if [[ "$phase" == fenced ]]; then fi if jq -e '.baseline!=null' "$work/journal.json" >/dev/null; then ensure_writer_barrier - refresh_objects; bind_current + refresh_objects + bind_current fi await natural-drain drain_receipts - plan drained; apply_ns; inspect + plan drained + apply_ns + inspect fi if [[ "$phase" == drained ]]; then controller_receipts - plan quiescing; apply_ns; inspect + plan quiescing + apply_ns + inspect fi if [[ "$phase" == quiescing ]]; then controller_receipts @@ -533,9 +586,15 @@ if [[ "$phase" == quiescing ]]; then kc -n arc-systems patch deployment arc-controller --type=json --patch-file "$work/controller-patch.json" >"$work/mutation.json" await retired-controller-processes controller_replaced await stable-zero-listener drain_receipts - plan quiesced; apply_ns; inspect + plan quiesced + apply_ns + inspect +fi +if [[ "$phase" == quiesced ]]; then + plan uninstalling + apply_ns + inspect fi -if [[ "$phase" == quiesced ]]; then plan uninstalling; apply_ns; inspect; fi if [[ "$phase" == uninstalling ]]; then refresh_objects bind_current @@ -543,11 +602,15 @@ if [[ "$phase" == uninstalling ]]; then delete_owned "$work/hr.json" /apis/helm.toolkit.fluxcd.io/v2/namespaces/arc-runners/helmreleases/platform-runners fi await chart-uninstall no_pool - plan uninstalled; apply_ns; inspect + plan uninstalled + apply_ns + inspect fi if [[ "$phase" == uninstalled ]]; then admission_receipts - plan credential-removing; apply_ns; inspect + plan credential-removing + apply_ns + inspect fi if [[ "$phase" == credential-removing ]]; then admission_receipts @@ -557,10 +620,13 @@ if [[ "$phase" == credential-removing ]]; then delete_owned "$work/eso.json" /apis/external-secrets.io/v1/namespaces/arc-runners/externalsecrets/arc-github-app fi await credential-finalization no_credential - plan absent; apply_ns; inspect + plan absent + apply_ns + inspect fi [[ "$phase" == absent || "$phase" == restored ]] || fail incomplete-retirement absence_receipts -refresh_objects; bind_current +refresh_objects +bind_current [[ "$children" == true && "$nodes" == true && "$secret" == true ]] || fail retirement-regressed printf 'ARC retirement finalized the pool and credential before publication; its closed fence remains.\n' diff --git a/scripts/run-openbao-backup-sandbox.sh b/scripts/run-openbao-backup-sandbox.sh index 44d94543db..9bc8270907 100755 --- a/scripts/run-openbao-backup-sandbox.sh +++ b/scripts/run-openbao-backup-sandbox.sh @@ -3,19 +3,22 @@ set -euo pipefail umask 077 -fail() { printf '%s\n' 'OPENBAO_BACKUP_RESTORE=FAIL stage=runtime' >&2; exit 2; } +fail() { + printf '%s\n' 'OPENBAO_BACKUP_RESTORE=FAIL stage=runtime' >&2 + exit 2 +} [[ $# == 3 && $(uname -s) == Linux && $(id -u) != 0 ]] || fail mode=$1 proof_dir=$2 binary=$3 record_count=3 case "$mode" in - verify|fixture|verify-email|fixture-email) ;; - verify-email-storage|fixture-email-storage) record_count=4 ;; + verify | fixture | verify-email | fixture-email) ;; + verify-email-storage | fixture-email-storage) record_count=4 ;; *) fail ;; esac fixture_mode=false -case "$mode" in fixture|fixture-email|fixture-email-storage) fixture_mode=true ;; esac +case "$mode" in fixture | fixture-email | fixture-email-storage) fixture_mode=true ;; esac [[ $proof_dir == /* && -d $proof_dir && ! -L $proof_dir && $binary == /* && -f $binary && ! -L $binary && -x $binary ]] || fail [[ $(cd "$proof_dir" && pwd -P) == "$proof_dir" && $(stat -c %a "$proof_dir") == 700 && $(stat -c %u "$proof_dir") == "$(id -u)" ]] || fail image=$("$binary" image) || fail @@ -33,9 +36,9 @@ cleanup() { [[ $rc != 0 ]] || rc=2 elif [[ -n $ids ]]; then if [[ $ids =~ ^[a-f0-9]{64}$ ]] && - label=$(docker inspect --type container --format '{{ index .Config.Labels "platform.devantler.tech/recovery-proof" }}' "$ids" 2>>"$proof_dir/cleanup.log") && - [[ $label == "$nonce" ]] && - docker rm --force "$ids" >"$proof_dir/cleanup.log" 2>&1; then + label=$(docker inspect --type container --format '{{ index .Config.Labels "platform.devantler.tech/recovery-proof" }}' "$ids" 2>>"$proof_dir/cleanup.log") && + [[ $label == "$nonce" ]] && + docker rm --force "$ids" >"$proof_dir/cleanup.log" 2>&1; then : else printf '%s\n' 'OPENBAO_BACKUP_CLEANUP=UNVERIFIED' >&2 diff --git a/scripts/run-publication-generator-tests.sh b/scripts/run-publication-generator-tests.sh index a6cbb98994..f30e10802c 100755 --- a/scripts/run-publication-generator-tests.sh +++ b/scripts/run-publication-generator-tests.sh @@ -16,7 +16,10 @@ tests=( test-canonical-bootstrap-output.sh test-canonical-publisher-regeneration.sh ) -die() { printf 'publication-tests: %s\n' "$*" >&2; exit 2; } +die() { + printf 'publication-tests: %s\n' "$*" >&2 + exit 2 +} # xargs appends only one validated index; it never chooses a command or path. # Capture all child exit codes, including 255, without telling xargs to stop @@ -40,7 +43,7 @@ fi workers=1 if [[ $# != 0 ]]; then [[ $# == 2 && $1 == --jobs ]] || die 'usage: run-publication-generator-tests.sh [--jobs 1|2|4]' - case "$2" in 1|2|4) workers="$2" ;; *) die 'worker limit must be 1, 2 or 4' ;; esac + case "$2" in 1 | 2 | 4) workers="$2" ;; *) die 'worker limit must be 1, 2 or 4' ;; esac fi command -v xargs >/dev/null 2>&1 || die 'xargs is required' for test_file in "${tests[@]}"; do @@ -61,7 +64,7 @@ for index in "${!tests[@]}"; do printf '::group::%s (exit %s, %ss)\n' "${tests[$index]}" "$result" "$seconds" cat "$work/$index.log" || die "cannot read output for ${tests[$index]}" printf '::endgroup::\n' - [[ "$result" == 0 ]] || failed=$((failed+1)) + [[ "$result" == 0 ]] || failed=$((failed + 1)) done [[ "$dispatch_status" == 0 ]] || die 'worker dispatch failed' printf 'PUBLICATION_TESTS jobs=%s count=%s failed=%s seconds=%s\n' "$workers" "${#tests[@]}" "$failed" "$SECONDS" diff --git a/scripts/tenant-publication-binding.lib.sh b/scripts/tenant-publication-binding.lib.sh index e2070f7a91..ea5f718748 100644 --- a/scripts/tenant-publication-binding.lib.sh +++ b/scripts/tenant-publication-binding.lib.sh @@ -151,7 +151,11 @@ business_site_manifest_evidence() ( case "$seen" in *":$option:"*) return 2 ;; esac seen="$seen$option:" case "$option" in - --verify-only) verify_only=1; shift; continue ;; + --verify-only) + verify_only=1 + shift + continue + ;; --consumer | --tag | --source-sha | --digest) [ "$#" -ge 2 ] || return 2 ;; *) return 2 ;; esac diff --git a/scripts/tenant-source-discovery.lib.sh b/scripts/tenant-source-discovery.lib.sh index e1ff251823..d6cc71a7b5 100644 --- a/scripts/tenant-source-discovery.lib.sh +++ b/scripts/tenant-source-discovery.lib.sh @@ -34,7 +34,8 @@ canonical_tenant_source_documents() ( # Every parser invocation proves its complete filename set, including the # Kustomize reference pass; a partial read can never narrow the later census. read_source_documents() { - local output="$1"; shift + local output="$1" + shift printf '%s\0' "$@" | jq -Rs 'split("\u0000")[:-1] | unique' >"$output.expected.json" || return 1 # YAML also accepts JSON. Older yq releases infer one input format for the # entire argument list, so mixed suffixes must never select that format. @@ -61,8 +62,10 @@ canonical_tenant_source_documents() ( case "$resource" in /*) resource_file="$resource" ;; esac resolved="$(realpath "$resource_file" 2>/dev/null)" || return 1 case "$resolved" in "$discovery_root/k8s" | "$discovery_root/k8s/"*) ;; *) return 1 ;; esac - if [ -f "$resolved" ]; then files+=("$resolved") - elif [ ! -d "$resolved" ]; then return 1 + if [ -f "$resolved" ]; then + files+=("$resolved") + elif [ ! -d "$resolved" ]; then + return 1 fi done <"$source_discovery_work/references" fi diff --git a/scripts/tests/lib/configure-hosted-registry-cache.sh b/scripts/tests/lib/configure-hosted-registry-cache.sh index 8b11c103bc..f4f8a464e9 100755 --- a/scripts/tests/lib/configure-hosted-registry-cache.sh +++ b/scripts/tests/lib/configure-hosted-registry-cache.sh @@ -5,19 +5,22 @@ set -euo pipefail if [ "$#" != 0 ]; then - printf 'FAIL: unexpected registry cache argument\n' >&2; exit 1; + printf 'FAIL: unexpected registry cache argument\n' >&2 + exit 1 fi if [ "${GITHUB_ACTIONS:-}" != true ] || [ "${RUNNER_ENVIRONMENT:-}" != github-hosted ] || [ "${RUNNER_OS:-}" != Linux ]; then - printf 'FAIL: registry cache setup requires a hosted Linux runner\n' >&2; exit 1; + printf 'FAIL: registry cache setup requires a hosted Linux runner\n' >&2 + exit 1 fi umask 077 cache_dir="$(mktemp -d)" trap 'rm -rf "${cache_dir}"' EXIT docker ps -aq >"${cache_dir}/containers" [ ! -s "${cache_dir}/containers" ] || { - printf 'FAIL: registry cache setup requires an unused Docker daemon\n' >&2; exit 1; + printf 'FAIL: registry cache setup requires an unused Docker daemon\n' >&2 + exit 1 } if sudo test -e /etc/docker/daemon.json; then sudo cat /etc/docker/daemon.json | tee "${cache_dir}/original.json" >/dev/null @@ -37,7 +40,8 @@ jq -e -s ' .["registry-mirrors"] = (["https://mirror.gcr.io"] + [(.["registry-mirrors"] // [])[] | select(. != "https://mirror.gcr.io")]) ' "${cache_dir}/original.json" >"${cache_dir}/daemon.json" 2>/dev/null || { - printf 'FAIL: invalid daemon or registry cache configuration\n' >&2; exit 1; + printf 'FAIL: invalid daemon or registry cache configuration\n' >&2 + exit 1 } sudo dockerd --validate --config-file="${cache_dir}/daemon.json" >/dev/null sudo mkdir -p /etc/docker diff --git a/scripts/tests/resolve-rendered-chart.sh b/scripts/tests/resolve-rendered-chart.sh index 1e89aedda3..3a4c47064c 100755 --- a/scripts/tests/resolve-rendered-chart.sh +++ b/scripts/tests/resolve-rendered-chart.sh @@ -6,7 +6,10 @@ set -euo pipefail release_file="${1:?HelmRelease JSON is required}" overlay_file="${2:?built overlay YAML is required}" -fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} definition="$(jq -r ' if .spec.chart != null and .spec.chartRef == null then "repository" diff --git a/scripts/tests/test-actual-budget-user-namespaces.sh b/scripts/tests/test-actual-budget-user-namespaces.sh index 44b4f0834d..052c8878a0 100755 --- a/scripts/tests/test-actual-budget-user-namespaces.sh +++ b/scripts/tests/test-actual-budget-user-namespaces.sh @@ -11,7 +11,10 @@ readonly scratch_dir trap 'rm -rf "${scratch_dir}"' EXIT # Print the supplied failure message to stderr and stop the regression. -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} for tool in kubectl yq jq helm; do command -v "${tool}" >/dev/null || fail "${tool} is required" diff --git a/scripts/tests/test-add-baseline-context.sh b/scripts/tests/test-add-baseline-context.sh index ceb192d0bf..3b0e8ddb3b 100755 --- a/scripts/tests/test-add-baseline-context.sh +++ b/scripts/tests/test-add-baseline-context.sh @@ -107,7 +107,6 @@ check podlevel-mutated.yaml '.spec.securityContext.seLinuxOptions.user' \ check podlevel-mutated.yaml '.spec.securityContext.fsGroupChangePolicy' \ OnRootMismatch "pod-level rule did not fire on the podlevel fixture" - # Partial pod options remain inherited as a whole; leave level selection to the # runtime and preserve the operator's user. The fsGroup check prevents vacuity. check podlevel-partial-mutated.yaml '.spec.securityContext.seLinuxOptions.level' \ diff --git a/scripts/tests/test-admit-coroot-baseline-context.sh b/scripts/tests/test-admit-coroot-baseline-context.sh index 72586a3079..ce64fdccd9 100755 --- a/scripts/tests/test-admit-coroot-baseline-context.sh +++ b/scripts/tests/test-admit-coroot-baseline-context.sh @@ -82,43 +82,84 @@ check() { fi printf 'ok: %s\n' "${name}" } -queue() { rm -f "${scratch}"/read.*.json; cp "${scratch}/$1.json" "${scratch}/read.json"; shift - local n=1; for f in "$@"; do cp "${scratch}/${f}.json" "${scratch}/read.${n}.json"; n=$((n + 1)); done; } +queue() { + rm -f "${scratch}"/read.*.json + cp "${scratch}/$1.json" "${scratch}/read.json" + shift + local n=1 + for f in "$@"; do + cp "${scratch}/${f}.json" "${scratch}/read.${n}.json" + n=$((n + 1)) + done +} queue hardened check 'templates that already carry both fields are not written' pass normal 'No admission write needed' -[[ ! -f "${scratch}/writes" && ! -f "${scratch}/rollouts" ]] || { echo 'FAIL: a hardened template was written' >&2; exit 1; } +[[ ! -f "${scratch}/writes" && ! -f "${scratch}/rollouts" ]] || { + echo 'FAIL: a hardened template was written' >&2 + exit 1 +} queue hardened unhardened hardened check 'every template lacking a field is written and rolled out' pass normal -[[ "$(wc -l <"${scratch}/writes")" -eq 6 ]] || { echo 'FAIL: expected six writes' >&2; exit 1; } +[[ "$(wc -l <"${scratch}/writes")" -eq 6 ]] || { + echo 'FAIL: expected six writes' >&2 + exit 1 +} grep -qx 'daemonset/coroot-node-agent' "${scratch}/writes" grep -qx 'statefulset/coroot-clickhouse-shard-0 --timeout=15m' "${scratch}/rollouts" -[[ "$(wc -l <"${scratch}/rollouts")" -eq 6 ]] || { echo 'FAIL: expected six rollouts' >&2; exit 1; } +[[ "$(wc -l <"${scratch}/rollouts")" -eq 6 ]] || { + echo 'FAIL: expected six rollouts' >&2 + exit 1 +} queue hardened one-lacking hardened check 'only the template lacking a field is written' pass normal -[[ "$(cat "${scratch}/writes")" == 'statefulset/coroot-coroot' ]] || { echo 'FAIL: wrong write set' >&2; exit 1; } -[[ "$(cat "${scratch}/rollouts")" == 'statefulset/coroot-coroot --timeout=15m' ]] || { echo 'FAIL: wrong rollout set' >&2; exit 1; } +[[ "$(cat "${scratch}/writes")" == 'statefulset/coroot-coroot' ]] || { + echo 'FAIL: wrong write set' >&2 + exit 1 +} +[[ "$(cat "${scratch}/rollouts")" == 'statefulset/coroot-coroot --timeout=15m' ]] || { + echo 'FAIL: wrong rollout set' >&2 + exit 1 +} queue hardened one-lacking agent-lacking hardened check 'a template that starts lacking a field during a retry is also rolled out' pass normal [[ "$(sort "${scratch}/rollouts" | paste -sd ' ' -)" == 'daemonset/coroot-node-agent --timeout=15m statefulset/coroot-coroot --timeout=15m' ]] || - { echo 'FAIL: a template written during a retry was not rolled out' >&2; exit 1; } + { + echo 'FAIL: a template written during a retry was not rolled out' >&2 + exit 1 + } queue seven check 'a population other than the reviewed six fails before any write' fail normal 'differ from the reviewed six' -[[ ! -f "${scratch}/writes" ]] || { echo 'FAIL: wrote to an unreviewed population' >&2; exit 1; } +[[ ! -f "${scratch}/writes" ]] || { + echo 'FAIL: wrote to an unreviewed population' >&2 + exit 1 +} queue hardened unhardened unhardened hardened check 'a write admitted before the label is visible is retried' pass normal -[[ "$(wc -l <"${scratch}/writes")" -eq 12 ]] || { echo 'FAIL: expected two write rounds' >&2; exit 1; } -[[ "$(wc -l <"${scratch}/rollouts")" -eq 6 ]] || { echo 'FAIL: expected six rollouts' >&2; exit 1; } +[[ "$(wc -l <"${scratch}/writes")" -eq 12 ]] || { + echo 'FAIL: expected two write rounds' >&2 + exit 1 +} +[[ "$(wc -l <"${scratch}/rollouts")" -eq 6 ]] || { + echo 'FAIL: expected six rollouts' >&2 + exit 1 +} queue unhardened check 'fields still missing after three writes fail before any rollout' fail normal 'admission did not add both fields' -[[ "$(wc -l <"${scratch}/writes")" -eq 18 ]] || { echo 'FAIL: expected three bounded write rounds' >&2; exit 1; } -[[ ! -f "${scratch}/rollouts" ]] || { echo 'FAIL: rolled out without admitted fields' >&2; exit 1; } +[[ "$(wc -l <"${scratch}/writes")" -eq 18 ]] || { + echo 'FAIL: expected three bounded write rounds' >&2 + exit 1 +} +[[ ! -f "${scratch}/rollouts" ]] || { + echo 'FAIL: rolled out without admitted fields' >&2 + exit 1 +} queue hardened unhardened hardened check 'an unfinished rollout fails' fail rollout-timeout 'did not finish within 15m' diff --git a/scripts/tests/test-alert-app-proof.sh b/scripts/tests/test-alert-app-proof.sh index 47bb3c5bfb..2cd24881f6 100755 --- a/scripts/tests/test-alert-app-proof.sh +++ b/scripts/tests/test-alert-app-proof.sh @@ -4,7 +4,10 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" workflow="$root/.github/workflows/verify-alert-publisher-app.yaml" -fail() { printf 'alert App proof guard: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'alert App proof guard: %s\n' "$1" >&2 + exit 1 +} [[ -f "$workflow" ]] || fail 'reviewed dispatch workflow is missing' [[ "$(yq -o=json -I=0 '.on | keys' "$workflow")" == '["workflow_dispatch"]' ]] || fail 'only manual dispatch is permitted' [[ "$(yq -o=json -I=0 '.jobs.proof.permissions' "$workflow")" == '{"contents":"read"}' ]] || fail 'workflow token must be read only' diff --git a/scripts/tests/test-arc-listener-public-content.sh b/scripts/tests/test-arc-listener-public-content.sh index 8bd051a10d..ba1dc764e1 100755 --- a/scripts/tests/test-arc-listener-public-content.sh +++ b/scripts/tests/test-arc-listener-public-content.sh @@ -7,7 +7,10 @@ policy=k8s/bases/infrastructure/cluster-policies/best-practices/restrict-arc-ope listener=k8s/providers/hetzner/infrastructure/controllers/openbao/transport/config-map-listener.yaml scratch=$(mktemp -d) trap 'rm -rf "$scratch"' EXIT -fail() { printf 'ARC public listener: FAIL %s\n' "$1" >&2; exit 1; } +fail() { + printf 'ARC public listener: FAIL %s\n' "$1" >&2 + exit 1 +} command -v ksail >/dev/null 2>&1 || fail missing-native-scanner yq -o=json '.' "$listener" >"$scratch/public.json" kyverno apply "$policy" --resource "$scratch/public.json" --remove-color >"$scratch/admission" 2>&1 || fail public-settings diff --git a/scripts/tests/test-arc-retirement-fence.sh b/scripts/tests/test-arc-retirement-fence.sh index edc346638c..b219b568dc 100755 --- a/scripts/tests/test-arc-retirement-fence.sh +++ b/scripts/tests/test-arc-retirement-fence.sh @@ -6,7 +6,10 @@ cd "$repo_root" policy=k8s/bases/infrastructure/cluster-policies/best-practices/restrict-arc-retirement.yaml scratch=$(mktemp -d) trap 'rm -rf "$scratch"' EXIT -fail() { printf 'ARC retirement admission: FAIL %s\n' "$1" >&2; exit 1; } +fail() { + printf 'ARC retirement admission: FAIL %s\n' "$1" >&2 + exit 1 +} [[ -f "$policy" ]] || fail missing-policy jq -n '{apiVersion:"helm.toolkit.fluxcd.io/v2",kind:"HelmRelease", metadata:{name:"platform-runners",namespace:"arc-runners"}, @@ -25,7 +28,7 @@ run_case() { jq -n --argjson groups "$groups" '{apiVersion:"cli.kyverno.io/v1alpha1",kind:"UserInfo",metadata:{name:"arc-retirement"}, userInfo:{username:"fixture",groups:$groups}}' >"$dir/userinfo.json" local code=200 - case "$flag" in 403|404|500) code=$flag ;; esac + case "$flag" in 403 | 404 | 500) code=$flag ;; esac jq -n --slurpfile resource "$dir/resource.json" --arg rule "$rule" --arg flag "$flag" --arg op "$operation" --arg expected "$expected" --argjson code "$code" '{ apiVersion:"cli.kyverno.io/v1alpha1",kind:"Test",metadata:{name:"arc-retirement"}, policies:["policy.yaml"],resources:["resource.json"],variables:"values.json",userinfo:"userinfo.json", @@ -35,7 +38,7 @@ run_case() { results:[{kind:$resource[0].kind,policy:"restrict-arc-retirement",rule:$rule, resources:[(($resource[0].metadata.namespace // "") + "/" + $resource[0].metadata.name | ltrimstr("/"))],operation:$op,result:$expected}] }' >"$dir/kyverno-test.yaml" - cases=$((cases+1)) + cases=$((cases + 1)) } run_case recreate-zero CREATE owned '.' fail run_case recreate-active CREATE owned '.spec.values.maxRunners=1' fail diff --git a/scripts/tests/test-authorization-gate-diagnostics.sh b/scripts/tests/test-authorization-gate-diagnostics.sh index adc745f9fc..d093d8aac7 100755 --- a/scripts/tests/test-authorization-gate-diagnostics.sh +++ b/scripts/tests/test-authorization-gate-diagnostics.sh @@ -223,7 +223,10 @@ assert_gate_behaviour() { for workflow_name in ci cd; do workflow="${root_dir}/.github/workflows/${workflow_name}.yaml" - [ -f "${workflow}" ] || { fail "${workflow_name}.yaml is missing"; continue; } + [ -f "${workflow}" ] || { + fail "${workflow_name}.yaml is missing" + continue + } script="${work_dir}/${workflow_name}-step.sh" extract_step "${workflow}" >"${script}" diff --git a/scripts/tests/test-auto-vpa-generating-policy-ownership.sh b/scripts/tests/test-auto-vpa-generating-policy-ownership.sh index d53381a656..d230b75902 100755 --- a/scripts/tests/test-auto-vpa-generating-policy-ownership.sh +++ b/scripts/tests/test-auto-vpa-generating-policy-ownership.sh @@ -20,7 +20,8 @@ for kind in deployment statefulset daemonset; do # The old owner must be rejected for the replacement with the same name. cp "$fixture/generated-vpa.yaml" "$case_dir/generated-vpa.yaml" if kyverno test "$case_dir" --remove-color >"$case_dir/old-owner.log" 2>&1; then - echo "FAIL: $kind accepted the previous incarnation"; exit 1 + echo "FAIL: $kind accepted the previous incarnation" + exit 1 fi for invalid in missing empty numeric; do cp "$fixture/resources.yaml" "$case_dir/resources.yaml" @@ -30,10 +31,13 @@ for kind in deployment statefulset daemonset; do numeric) yq -i '.metadata.uid = 42' "$case_dir/resources.yaml" ;; esac if kyverno test "$case_dir" --remove-color >"$case_dir/$invalid.log" 2>&1; then - echo "FAIL: $kind accepted a $invalid UID"; exit 1 + echo "FAIL: $kind accepted a $invalid UID" + exit 1 fi if ! grep -Eqi 'error|failed|not found' "$case_dir/$invalid.log"; then - cat "$case_dir/$invalid.log"; echo "FAIL: $kind/$invalid failed without evaluation evidence"; exit 1 + cat "$case_dir/$invalid.log" + echo "FAIL: $kind/$invalid failed without evaluation evidence" + exit 1 fi done echo "ok: $kind binds the replacement UID and refuses missing, empty and numeric UIDs" diff --git a/scripts/tests/test-business-disclosure-reader.sh b/scripts/tests/test-business-disclosure-reader.sh index c25f3521ef..24d91313df 100755 --- a/scripts/tests/test-business-disclosure-reader.sh +++ b/scripts/tests/test-business-disclosure-reader.sh @@ -9,7 +9,10 @@ readonly binary_dir trap 'rm -f "${binary_dir}/check"; rmdir "${binary_dir}"' EXIT (cd "${root_dir}" && GOTOOLCHAIN=local go build -o "${binary_dir}/check" ./scripts/check-business-disclosure-reader) -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} rendered="$(kubectl kustomize "${root_dir}/k8s/bases/infrastructure/vault-config")" config_script="$(yq -r ' diff --git a/scripts/tests/test-business-staging-hold.sh b/scripts/tests/test-business-staging-hold.sh index cd56a81f19..e06cf17665 100755 --- a/scripts/tests/test-business-staging-hold.sh +++ b/scripts/tests/test-business-staging-hold.sh @@ -5,8 +5,8 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' EXIT -kubectl kustomize "$root/k8s/providers/hetzner/apps" > "$scratch/production.yaml" -yq -o=json -I=0 '.' "$scratch/production.yaml" | jq -s '.' > "$scratch/production.json" +kubectl kustomize "$root/k8s/providers/hetzner/apps" >"$scratch/production.yaml" +yq -o=json -I=0 '.' "$scratch/production.yaml" | jq -s '.' >"$scratch/production.json" check_hold() { jq -e ' [ .[] | select(.kind == "CiliumNetworkPolicy" and .metadata.name == "business-site-staging-hold") ] | @@ -17,7 +17,10 @@ check_hold() { } ' "$1" >/dev/null } -check_hold "$scratch/production.json" || { echo 'missing or broadened staging ingress hold' >&2; exit 1; } +check_hold "$scratch/production.json" || { + echo 'missing or broadened staging ingress hold' >&2 + exit 1 +} # These controls prevent an empty selector, a changed port/direction, or a mere # additive allow policy from satisfying the independently enforced hold. for change in \ @@ -28,8 +31,11 @@ for change in \ 'map(if .metadata.name == "business-site-staging-hold" then .spec.ingressDeny[0].fromEntities=["all"] else . end)' \ 'map(if .metadata.name == "business-site-staging-hold" then .spec.ingressDeny[0].toPorts[0].ports[0].port="80" else . end)' \ 'map(if .metadata.name == "business-site-staging-hold" then .spec.egress=[{}] else . end)'; do - jq "$change" "$scratch/production.json" > "$scratch/changed.json" - if check_hold "$scratch/changed.json"; then echo 'unsafe hold mutation was accepted' >&2; exit 1; fi + jq "$change" "$scratch/production.json" >"$scratch/changed.json" + if check_hold "$scratch/changed.json"; then + echo 'unsafe hold mutation was accepted' >&2 + exit 1 + fi done # Installing the hold is deliberately independent of enabling the application. jq -e ' @@ -38,6 +44,6 @@ jq -e ' .[0].spec.webApp == false and .[0].spec.externalDns == false and .[0].spec.source.reconcileApplication == false ' "$scratch/production.json" >/dev/null -kubectl kustomize "$root/k8s/providers/docker/apps" > "$scratch/local.yaml" +kubectl kustomize "$root/k8s/providers/docker/apps" >"$scratch/local.yaml" yq -o=json -I=0 '.' "$scratch/local.yaml" | jq -es 'all(.[]; .metadata.name != "business-site-staging-hold")' >/dev/null printf 'PASS: production-only staging hold; seven unsafe mutations refused; application remains disabled\n' diff --git a/scripts/tests/test-canonical-bootstrap-output.sh b/scripts/tests/test-canonical-bootstrap-output.sh index 9445d2af93..e1bcf8f613 100755 --- a/scripts/tests/test-canonical-bootstrap-output.sh +++ b/scripts/tests/test-canonical-bootstrap-output.sh @@ -16,7 +16,7 @@ jq '.bindings[0] | {schemaVersion:1,kind:"manifest-publication-evidence",consume gitTag:.publication.gitTag,sourceSHA:.publication.sourceSHA,digest:.sourceIdentity.digest, publisherIdentity:.publication.publisherIdentity,callerWorkflow:.publication.callerWorkflow,event:.publication.event, workflowRunId:.publication.workflowRunId,workflowRunAttempt:.publication.workflowRunAttempt}' \ - "$test_root/scripts/canonical-tenant-source-bindings.json" >"$test_work/receipt.json" + "$test_root/scripts/canonical-tenant-source-bindings.json" >"$test_work/receipt.json" # shellcheck source=scripts/bootstrap-tenant-source.sh source "$test_root/scripts/bootstrap-tenant-source.sh" business_site_manifest_evidence() { cat "$test_work/receipt.json"; } @@ -25,7 +25,10 @@ kubectl() { directory) mkdir "$fixture_output" ;; symlink) ln -s "$test_work/redirect" "$fixture_output" ;; conflict) printf '{"unreviewed":true}\n' >"$fixture_output" ;; - observed) jq '.bindings[0].kind="published-binding"' "$fixture_output" >"$fixture_output.changed"; mv "$fixture_output.changed" "$fixture_output" ;; + observed) + jq '.bindings[0].kind="published-binding"' "$fixture_output" >"$fixture_output.changed" + mv "$fixture_output.changed" "$fixture_output" + ;; esac printf '{"apiVersion":"v1","kind":"List","metadata":{},"items":[]}\n' } @@ -37,15 +40,31 @@ run_admission() { --digest "$(jq -r .digest "$test_work/receipt.json")" } printf '{"schemaVersion":1,"bindings":[]}\n' >"$fixture_output" -if run_admission >"$test_work/result" 2>&1; then echo 'empty registry reported an unchanged admission' >&2; exit 1; fi +if run_admission >"$test_work/result" 2>&1; then + echo 'empty registry reported an unchanged admission' >&2 + exit 1 +fi test "$(jq '.bindings|length' "$fixture_output")" = 0 rm "$fixture_output" for race in directory symlink conflict; do - if run_admission >"$test_work/result" 2>&1; then echo "output $race race reported success" >&2; exit 1; fi + if run_admission >"$test_work/result" 2>&1; then + echo "output $race race reported success" >&2 + exit 1 + fi case "$race" in - directory) test -z "$(find "$fixture_output" -mindepth 1 -print)"; rmdir "$fixture_output" ;; - symlink) test -L "$fixture_output"; test -z "$(find "$test_work/redirect" -mindepth 1 -print)"; rm "$fixture_output" ;; - conflict) grep -qxF '{"unreviewed":true}' "$fixture_output"; rm "$fixture_output" ;; + directory) + test -z "$(find "$fixture_output" -mindepth 1 -print)" + rmdir "$fixture_output" + ;; + symlink) + test -L "$fixture_output" + test -z "$(find "$test_work/redirect" -mindepth 1 -print)" + rm "$fixture_output" + ;; + conflict) + grep -qxF '{"unreviewed":true}' "$fixture_output" + rm "$fixture_output" + ;; esac done race='' @@ -55,6 +74,9 @@ cp "$fixture_output" "$test_work/first.json" run_admission >"$test_work/result" cmp "$test_work/first.json" "$fixture_output" race=observed -if run_admission >"$test_work/result" 2>&1; then echo 'concurrent observed record was overwritten' >&2; exit 1; fi +if run_admission >"$test_work/result" 2>&1; then + echo 'concurrent observed record was overwritten' >&2 + exit 1 +fi test "$(jq -r '.bindings[0].kind' "$fixture_output")" = published-binding printf 'Canonical bootstrap output: empty registry and four races refused; exclusive creation/idempotence passed.\n' diff --git a/scripts/tests/test-canonical-constructor-attribution.sh b/scripts/tests/test-canonical-constructor-attribution.sh index e8d1d8b24e..639fba993e 100755 --- a/scripts/tests/test-canonical-constructor-attribution.sh +++ b/scripts/tests/test-canonical-constructor-attribution.sh @@ -32,7 +32,7 @@ yq '.metadata.name="unadmitted-tenant.kro.run" | (.spec.resources[] | select(.id accepted=0 if discover >"$work/raw" 2>"$work/raw-error"; then printf 'raw discovery attributed a Tenant to an unrelated-group graph\n' >&2 - accepted=$((accepted+1)) + accepted=$((accepted + 1)) fi test ! -s "$work/raw" || test "$accepted" -gt 0 # Exercise the rendered reader independently with its complete parser envelope. @@ -43,7 +43,7 @@ yq -N -o=json -I=0 '{"file":filename,"document":explode(.)}' "$tree/$graph" "$tr source "$attribution_repo_root/scripts/canonical-tenant-source.lib.sh" canonical_typed_source_hits "$tree" "$work/inventory.jsonl" >"$work/hits.json" canonical_tenant_inventory_projection "$tree" "$work/inventory.jsonl" "$work/hits.json" rendered -) >"$work/rendered" 2>"$work/rendered-error" && accepted=$((accepted+1)) +) >"$work/rendered" 2>"$work/rendered-error" && accepted=$((accepted + 1)) if [ "$accepted" -ne 0 ]; then printf 'constructor attribution accepted %s invalid raw/rendered projections\n' "$accepted" >&2 exit 1 diff --git a/scripts/tests/test-canonical-generator-output.sh b/scripts/tests/test-canonical-generator-output.sh index fe75102d78..88d2c0dbfb 100755 --- a/scripts/tests/test-canonical-generator-output.sh +++ b/scripts/tests/test-canonical-generator-output.sh @@ -78,7 +78,8 @@ grep -qxF 'new record' "$generator_test_work/target" printf 'still pending\n' >"$generator_test_work/pending" mkdir "$generator_test_work/directory" if "$generator_test_work/run" replace "$generator_test_work/pending" "$generator_test_work/directory" >"$generator_test_work/replace-error" 2>&1; then - printf 'replacement interpreted a directory as an output container\n' >&2; exit 1 + printf 'replacement interpreted a directory as an output container\n' >&2 + exit 1 fi test -z "$(find "$generator_test_work/directory" -mindepth 1 -print)" grep -qxF 'still pending' "$generator_test_work/pending" diff --git a/scripts/tests/test-canonical-publisher-regeneration.sh b/scripts/tests/test-canonical-publisher-regeneration.sh index 4e328b36c9..d01d3e357a 100755 --- a/scripts/tests/test-canonical-publisher-regeneration.sh +++ b/scripts/tests/test-canonical-publisher-regeneration.sh @@ -9,7 +9,10 @@ fi case "${2:-}" in manifests) export CANONICAL_TEST_CONSUMER=.github CANONICAL_TEST_WORKFLOW=publish-manifests ;; app) export CANONICAL_TEST_CONSUMER=ascoachingogvaner CANONICAL_TEST_WORKFLOW=publish-app ;; - *) echo 'Unknown publisher fixture family' >&2; exit 1 ;; + *) + echo 'Unknown publisher fixture family' >&2 + exit 1 + ;; esac test_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" test_work="$(mktemp -d)" @@ -25,9 +28,15 @@ awk '/^lookup\(\) \{$/ {copy=1} copy {print} copy && /^}$/ {exit}' \ source "$test_work/lookup.sh" records=$(awk 'BEGIN {print "wanted\tfirst-record"; for (i=0;i<20000;i++) printf "filler-%05d\t%080d\n",i,i; print "wanted\tsecond-record"}') actual=$(lookup "$records" wanted) - [[ "$actual" == first-record ]] || { echo 'Lookup did not preserve the first match' >&2; exit 1; } + [[ "$actual" == first-record ]] || { + echo 'Lookup did not preserve the first match' >&2 + exit 1 + } actual=$(lookup $'100\tdecimal\n1e2\texponent' 1e2) - [[ "$actual" == exponent ]] || { echo 'Lookup compared numeric-looking keys as numbers' >&2; exit 1; } + [[ "$actual" == exponent ]] || { + echo 'Lookup compared numeric-looking keys as numbers' >&2 + exit 1 + } ) # shellcheck source=scripts/tests/lib/legacy-consumer-fixture.sh source "$test_root/scripts/tests/lib/legacy-consumer-fixture.sh" @@ -47,7 +56,10 @@ if [[ "$CANONICAL_TEST_WORKFLOW" == publish-app ]]; then printf '%s\t%s\tdevantler-tech/.github\t%040d\n' "$CANONICAL_TEST_CONSUMER" "$CANONICAL_TEST_WORKFLOW" 6 >>"$CANONICAL_APPROVALS_FILE" fi CANONICAL_TEST_SHA="$(awk -F'\t' -v consumer="$CANONICAL_TEST_CONSUMER" -v workflow="$CANONICAL_TEST_WORKFLOW" '$1==consumer && $2==workflow {print $4; exit}' "$CANONICAL_APPROVALS_FILE")" -[[ "$CANONICAL_TEST_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo 'The fixture approves no matching canonical commit' >&2; exit 1; } +[[ "$CANONICAL_TEST_SHA" =~ ^[0-9a-f]{40}$ ]] || { + echo 'The fixture approves no matching canonical commit' >&2 + exit 1 +} export CANONICAL_TEST_SHA mkdir -p "$test_work/bin" cp "$test_root/scripts/publish-workflow-approved-revisions.tsv" "$test_work/seed.tsv" @@ -57,7 +69,10 @@ while IFS=$'\t' read -r consumer workflow _rest; do if ! awk -F'\t' -v consumer="$consumer" '$1==consumer {found=1} END {exit !found}' "$test_work/seed.tsv"; then template=$(awk -F'\t' -v consumer="$consumer" -v workflow="$workflow" 'BEGIN {OFS="\t"} NR>1 && $2==workflow {$1=consumer; print; exit}' "$test_work/seed.tsv") - [[ -n "$template" ]] || { echo 'No observed-row fixture for the registered workflow' >&2; exit 1; } + [[ -n "$template" ]] || { + echo 'No observed-row fixture for the registered workflow' >&2 + exit 1 + } printf '%s\n' "$template" >>"$test_work/seed.tsv" fi done < <("$test_root/scripts/report-publish-workflow-signing-revisions.sh" --list-consumers) @@ -117,26 +132,37 @@ chmod +x "$test_work/bin/gh" "$test_work/observe" "$test_work/pin" "$test_work/r expected_identity="devantler-tech/.github/.github/workflows/$CANONICAL_TEST_WORKFLOW.yaml@$CANONICAL_TEST_SHA" answer="$(PATH="$test_work/bin:$PATH" bash -c 'source "$1"; report_publisher_at_ref "$CANONICAL_TEST_CONSUMER" "$CANONICAL_TEST_WORKFLOW" main' bash "$test_root/scripts/report-publish-workflow-signing-revisions.sh")" || { - echo "Canonical report resolution failed for $CANONICAL_TEST_WORKFLOW" >&2; exit 1; + echo "Canonical report resolution failed for $CANONICAL_TEST_WORKFLOW" >&2 + exit 1 +} +[[ "$answer" == "$expected_identity" ]] || { + echo 'Canonical report identity was lost' >&2 + exit 1 } -[[ "$answer" == "$expected_identity" ]] || { echo 'Canonical report identity was lost' >&2; exit 1; } answer="$(bash -c 'source "$1"; report_publisher_identity "$2" "$CANONICAL_TEST_WORKFLOW"' bash "$test_root/scripts/report-publish-workflow-signing-revisions.sh" "$expected_identity")" || { - echo 'Canonical identity validation failed' >&2; exit 1; + echo 'Canonical identity validation failed' >&2 + exit 1 +} +[[ "$answer" == "$expected_identity" ]] || { + echo 'Canonical identity changed during validation' >&2 + exit 1 } -[[ "$answer" == "$expected_identity" ]] || { echo 'Canonical identity changed during validation' >&2; exit 1; } if bash -c 'source "$1"; report_publisher_identity "$2" publish-other' bash "$test_root/scripts/report-publish-workflow-signing-revisions.sh" "$expected_identity" >"$test_work/answer"; then - echo 'An unsupported publisher workflow was accepted' >&2; exit 1 + echo 'An unsupported publisher workflow was accepted' >&2 + exit 1 fi [[ ! -s "$test_work/answer" ]] answer="$(PATH="$test_work/bin:$PATH" bash -c 'source "$1"; signer_for_tag "$CANONICAL_TEST_CONSUMER" "$CANONICAL_TEST_WORKFLOW" v7.9.2' bash "$test_root/scripts/generate-publish-workflow-approved-revisions.sh")" [[ "$answer" == "devantler-tech/.github/.github/workflows/$CANONICAL_TEST_WORKFLOW.yaml@$CANONICAL_TEST_SHA" ]] || { - echo 'Canonical signing identity was lost' >&2; exit 1; + echo 'Canonical signing identity was lost' >&2 + exit 1 } for variation in incomplete-page ambiguous-family mismatched-ref unknown-family; do if CANONICAL_RUN_CASE="$variation" PATH="$test_work/bin:$PATH" bash -c 'source "$1"; signer_for_tag "$CANONICAL_TEST_CONSUMER" "$CANONICAL_TEST_WORKFLOW" v7.9.2' bash "$test_root/scripts/generate-publish-workflow-approved-revisions.sh" >"$test_work/answer" 2>"$test_work/refusal"; then - echo "Unsafe publication evidence accepted: $variation" >&2; exit 1 + echo "Unsafe publication evidence accepted: $variation" >&2 + exit 1 fi [[ ! -s "$test_work/answer" ]] done @@ -150,8 +176,8 @@ assert_not_approved_refusal() { } run_generator() { APPROVED_REVISIONS_FILE="$test_work/approved.tsv" \ - APPROVED_REVISION_OBSERVER="$test_work/observe" APPROVED_REVISION_PIN_RESOLVER="$test_work/pin" \ - APPROVED_REVISION_RELEASE_RESOLVER="$test_work/release" \ + APPROVED_REVISION_OBSERVER="$test_work/observe" APPROVED_REVISION_PIN_RESOLVER="$test_work/pin" \ + APPROVED_REVISION_RELEASE_RESOLVER="$test_work/release" \ bash "$test_root/scripts/generate-publish-workflow-approved-revisions.sh" } assert_identity_row() { @@ -163,7 +189,8 @@ assert_identity_row() { NR==1 {if ($0!=header) bad=1; next} $1==consumer && $2==workflow {rows++; if (NF!=7 || $5!=applied || $6!=pinned) bad=1} END {exit (bad || rows!=1)}' "$file" || { - echo 'Canonical identity row mismatch' >&2; return 1; + echo 'Canonical identity row mismatch' >&2 + return 1 } } run_generator @@ -183,7 +210,8 @@ for mutation in consumer applied pinned duplicate; do {print}' "$test_work/approved.identities.tsv" >"$test_work/wrong-row.tsv" grep -qF "$expected_identity" "$test_work/wrong-row.tsv" if assert_identity_row "$test_work/wrong-row.tsv" "$CANONICAL_TEST_SHA" >"$test_work/row-refusal" 2>&1; then - echo "Incorrect identity row was admitted: $mutation" >&2; exit 1 + echo "Incorrect identity row was admitted: $mutation" >&2 + exit 1 fi grep -qF 'Canonical identity row mismatch' "$test_work/row-refusal" done @@ -194,11 +222,16 @@ cmp "$test_work/identities-before" "$test_work/approved.identities.tsv" # Every commit the record approves for this consumer is admitted as its current pin: while a # publisher moves from one reviewed commit to the next, the record holds both (#4713). approved_commits="$(awk -F'\t' -v consumer="$CANONICAL_TEST_CONSUMER" -v workflow="$CANONICAL_TEST_WORKFLOW" '$1==consumer && $2==workflow {print $4}' "$CANONICAL_APPROVALS_FILE")" -[[ -n "$approved_commits" ]] || { echo 'The fixture approves no matching commit; the cases below would prove nothing' >&2; exit 1; } +[[ -n "$approved_commits" ]] || { + echo 'The fixture approves no matching commit; the cases below would prove nothing' >&2 + exit 1 +} while IFS= read -r approved_commit; do export CANONICAL_PIN_SHA="$approved_commit" if ! run_generator >"$test_work/admitted" 2>&1; then - echo "Approved canonical pin $approved_commit was refused" >&2; cat "$test_work/admitted" >&2; exit 1 + echo "Approved canonical pin $approved_commit was refused" >&2 + cat "$test_work/admitted" >&2 + exit 1 fi assert_identity_row "$test_work/approved.identities.tsv" "$approved_commit" cp "$test_work/seed.tsv" "$test_work/approved.tsv" @@ -208,7 +241,8 @@ unset CANONICAL_PIN_SHA export CANONICAL_PIN_SHA=7777777777777777777777777777777777777777 if run_generator >"$test_work/refusal" 2>&1; then - echo 'Unapproved canonical current pin was admitted' >&2; exit 1 + echo 'Unapproved canonical current pin was admitted' >&2 + exit 1 fi assert_not_approved_refusal cmp "$test_work/seed.tsv" "$test_work/approved.tsv" @@ -218,7 +252,8 @@ unset CANONICAL_PIN_SHA # An unapproved canonical commit must not refresh either output. export CANONICAL_TEST_SHA=7777777777777777777777777777777777777777 if run_generator >"$test_work/refusal" 2>&1; then - echo 'Unapproved canonical signer was admitted' >&2; exit 1 + echo 'Unapproved canonical signer was admitted' >&2 + exit 1 fi assert_not_approved_refusal cmp "$test_work/seed.tsv" "$test_work/approved.tsv" diff --git a/scripts/tests/test-canonical-source-bootstrap.sh b/scripts/tests/test-canonical-source-bootstrap.sh index c20ca8f9e2..b1f8e0e893 100755 --- a/scripts/tests/test-canonical-source-bootstrap.sh +++ b/scripts/tests/test-canonical-source-bootstrap.sh @@ -61,47 +61,52 @@ awk -F '\t' '$1=="business-site" && $2=="publish-app" && $8=="oci://ghcr.io/devantler-tech/business-site/manifests" {ok=1} END {exit !ok}' "$work/rows" controls=0 for mutation in \ - '.bindings[0].consumer="different"' \ - '.bindings[0].consumerRepository="other/business-site"' \ - '.bindings[0].workflow="publish-manifests"' \ - '.bindings[0].kind="unknown"' \ - '.bindings[0].extra=true' \ - '.bindings += .bindings' \ - '.bindings[0].sourcePath="../outside.yaml"' \ - '.bindings[0].constructorSpecSha256=("0"*64)' \ - '.bindings[0].tenantIdentity.kind="OtherTenant"' \ - '.bindings[0].tenantIdentity.namespace="other"' \ - '.bindings[0].sourceIdentity.name="other"' \ - '.bindings[0].sourceIdentity.artifactURL="oci://ghcr.io/other/business-site/manifests"' \ - '.bindings[0].sourceIdentity.digest="sha256:invalid"' \ - '.bindings[0].publication.gitTag="not-a-tag"' \ - '.bindings[0].publication.sourceSHA="partial"' \ - '.bindings[0].publication.publisherIdentity="devantler-tech/actions/.github/workflows/publish-app.yaml@467f07582436a26b62cf4aa756423512f85ba0c0"' \ - '.bindings[0].publication.callerWorkflow=".github/workflows/cd.yaml"' \ - '.bindings[0].publication.event="push"' \ - '.bindings[0].publication.workflowRunId=0' \ - '.bindings[0].publication.workflowRunAttempt="1"'; do + '.bindings[0].consumer="different"' \ + '.bindings[0].consumerRepository="other/business-site"' \ + '.bindings[0].workflow="publish-manifests"' \ + '.bindings[0].kind="unknown"' \ + '.bindings[0].extra=true' \ + '.bindings += .bindings' \ + '.bindings[0].sourcePath="../outside.yaml"' \ + '.bindings[0].constructorSpecSha256=("0"*64)' \ + '.bindings[0].tenantIdentity.kind="OtherTenant"' \ + '.bindings[0].tenantIdentity.namespace="other"' \ + '.bindings[0].sourceIdentity.name="other"' \ + '.bindings[0].sourceIdentity.artifactURL="oci://ghcr.io/other/business-site/manifests"' \ + '.bindings[0].sourceIdentity.digest="sha256:invalid"' \ + '.bindings[0].publication.gitTag="not-a-tag"' \ + '.bindings[0].publication.sourceSHA="partial"' \ + '.bindings[0].publication.publisherIdentity="devantler-tech/actions/.github/workflows/publish-app.yaml@467f07582436a26b62cf4aa756423512f85ba0c0"' \ + '.bindings[0].publication.callerWorkflow=".github/workflows/cd.yaml"' \ + '.bindings[0].publication.event="push"' \ + '.bindings[0].publication.workflowRunId=0' \ + '.bindings[0].publication.workflowRunAttempt="1"'; do jq "$mutation" "$work/good.json" >"$tree/scripts/canonical-tenant-source-bindings.json" if discover >"$work/rows" 2>"$work/error"; then - echo "invalid canonical binding admitted: $mutation" >&2; exit 1 + echo "invalid canonical binding admitted: $mutation" >&2 + exit 1 fi test ! -s "$work/rows" - controls=$((controls+1)) + controls=$((controls + 1)) done cp "$work/good.json" "$tree/scripts/canonical-tenant-source-bindings.json" for mutation in '.spec.source.reconcileApplication=true' '.spec.webApp=true' '.spec.externalDns=true' '.spec.sops=true' '.spec.extra="unreviewed"'; do yq "$mutation" "$work/good-tenant.yaml" >"$tree/$source_path" if discover >"$work/rows" 2>"$work/error"; then - echo "changed source-only instance admitted: $mutation" >&2; exit 1 + echo "changed source-only instance admitted: $mutation" >&2 + exit 1 fi test ! -s "$work/rows" - controls=$((controls+1)) + controls=$((controls + 1)) done cp "$work/good-tenant.yaml" "$tree/$source_path" cp "$tree/$source_path" "$tree/$(dirname "$source_path")/clone.yaml" -if discover >"$work/rows" 2>"$work/error"; then echo 'cloned Tenant admitted' >&2; exit 1; fi +if discover >"$work/rows" 2>"$work/error"; then + echo 'cloned Tenant admitted' >&2 + exit 1 +fi test ! -s "$work/rows" -printf 'Canonical source bootstrap: positive discovery and %d binding/instance refusal controls passed.\n' "$((controls+1))" +printf 'Canonical source bootstrap: positive discovery and %d binding/instance refusal controls passed.\n' "$((controls + 1))" # The production guard must independently attribute the real rendered graph. # A passing raw parser cannot substitute for this actual Kustomize comparison. @@ -215,22 +220,41 @@ BASH chmod 755 "$work/bootstrap-fixture" rm "$production/scripts/canonical-tenant-source-bindings.json" for race in directory symlink conflict; do - if env CANONICAL_BOOT_RACE="$race" "$work/bootstrap-fixture" >"$work/error" 2>&1; then echo "bootstrap accepted destination $race race" >&2; exit 1; fi + if env CANONICAL_BOOT_RACE="$race" "$work/bootstrap-fixture" >"$work/error" 2>&1; then + echo "bootstrap accepted destination $race race" >&2 + exit 1 + fi case "$race" in - directory) test -z "$(find "$production/scripts/canonical-tenant-source-bindings.json" -mindepth 1 -print)"; rmdir "$production/scripts/canonical-tenant-source-bindings.json" ;; - symlink) test -L "$production/scripts/canonical-tenant-source-bindings.json"; test -z "$(find "$work/redirect" -mindepth 1 -print)"; rm "$production/scripts/canonical-tenant-source-bindings.json" ;; - conflict) grep -qxF '{"unreviewed":true}' "$production/scripts/canonical-tenant-source-bindings.json"; rm "$production/scripts/canonical-tenant-source-bindings.json" ;; + directory) + test -z "$(find "$production/scripts/canonical-tenant-source-bindings.json" -mindepth 1 -print)" + rmdir "$production/scripts/canonical-tenant-source-bindings.json" + ;; + symlink) + test -L "$production/scripts/canonical-tenant-source-bindings.json" + test -z "$(find "$work/redirect" -mindepth 1 -print)" + rm "$production/scripts/canonical-tenant-source-bindings.json" + ;; + conflict) + grep -qxF '{"unreviewed":true}' "$production/scripts/canonical-tenant-source-bindings.json" + rm "$production/scripts/canonical-tenant-source-bindings.json" + ;; esac done "$work/bootstrap-fixture" >"$work/bootstrap-generation" cp "$production/scripts/canonical-tenant-source-bindings.json" "$work/bootstrap-generated.json" -if env CANONICAL_BOOT_RACE=observed "$work/bootstrap-fixture" >"$work/error" 2>&1; then echo 'bootstrap overwrote concurrent observed binding' >&2; exit 1; fi +if env CANONICAL_BOOT_RACE=observed "$work/bootstrap-fixture" >"$work/error" 2>&1; then + echo 'bootstrap overwrote concurrent observed binding' >&2 + exit 1 +fi test "$(jq -r '.bindings[0].kind' "$production/scripts/canonical-tenant-source-bindings.json")" = published-binding cp "$work/bootstrap-generated.json" "$production/scripts/canonical-tenant-source-bindings.json" "$work/bootstrap-fixture" >"$work/bootstrap-generation" cmp "$work/bootstrap-generated.json" "$production/scripts/canonical-tenant-source-bindings.json" for failure in CANONICAL_VERIFY_FAILURE CANONICAL_INVENTORY_FAILURE; do - if env "$failure=1" "$work/bootstrap-fixture" >"$work/error" 2>&1; then echo "bootstrap admitted failed $failure" >&2; exit 1; fi + if env "$failure=1" "$work/bootstrap-fixture" >"$work/error" 2>&1; then + echo "bootstrap admitted failed $failure" >&2 + exit 1 + fi cmp "$work/bootstrap-generated.json" "$production/scripts/canonical-tenant-source-bindings.json" done @@ -245,7 +269,10 @@ jq '.items[0].status={}' "$work/observed-inventory.json" >"$work/source-inventor "$work/generate-fixture" >"$work/generation" test "$(jq -r '.bindings[0].kind' "$production/scripts/canonical-tenant-source-bindings.json")" = published-bootstrap cp "$work/observed-inventory.json" "$work/source-inventory.json" -if "$work/bootstrap-fixture" >"$work/error" 2>&1; then echo 'bootstrap replaced real observation' >&2; exit 1; fi +if "$work/bootstrap-fixture" >"$work/error" 2>&1; then + echo 'bootstrap replaced real observation' >&2 + exit 1 +fi "$work/generate-fixture" >"$work/generation" test "$(jq -r '.bindings[0].kind' "$production/scripts/canonical-tenant-source-bindings.json")" = published-binding test "$(awk -F '\t' '$1=="business-site" && NF==7 {n++} END {print n+0}' "$production/scripts/publish-workflow-approved-revisions.identities.tsv")" = 1 @@ -256,28 +283,40 @@ APPROVED_REVISIONS_OBSERVED_ON=2030-01-01 "$work/generate-fixture" >"$work/gener cmp "$work/observed-identities.tsv" "$production/scripts/publish-workflow-approved-revisions.identities.tsv" cmp "$work/observed-binding.json" "$production/scripts/canonical-tenant-source-bindings.json" for mutation in '.items=[]' '.metadata.continue="next"' '.items += .items' \ - '.items=[{}]' '.metadata.continue=123' '.items[0].status.conditions="malformed"' \ - '.items[0].status={}' '.items[0].status.observedGeneration=1' \ - '.items[0].status.conditions[1].status="False"' '.items[0].status.conditions[0].observedGeneration=1' \ - '.items[0].spec.url="oci://ghcr.io/other/manifests"' '.items[0].status.artifact.revision="sha256:wrong"'; do + '.items=[{}]' '.metadata.continue=123' '.items[0].status.conditions="malformed"' \ + '.items[0].status={}' '.items[0].status.observedGeneration=1' \ + '.items[0].status.conditions[1].status="False"' '.items[0].status.conditions[0].observedGeneration=1' \ + '.items[0].spec.url="oci://ghcr.io/other/manifests"' '.items[0].status.artifact.revision="sha256:wrong"'; do jq "$mutation" "$work/observed-inventory.json" >"$work/source-inventory.json" - if "$work/generate-fixture" >"$work/error" 2>&1; then echo "observed source regressed: $mutation" >&2; exit 1; fi + if "$work/generate-fixture" >"$work/error" 2>&1; then + echo "observed source regressed: $mutation" >&2 + exit 1 + fi cmp "$work/observed-identities.tsv" "$production/scripts/publish-workflow-approved-revisions.identities.tsv" cmp "$work/observed-binding.json" "$production/scripts/canonical-tenant-source-bindings.json" cmp "$work/legacy-before.tsv" "$production/scripts/publish-workflow-approved-revisions.tsv" done cp "$work/observed-inventory.json" "$work/source-inventory.json" for failure in CANONICAL_VERIFY_FAILURE CANONICAL_INVENTORY_FAILURE; do - if env "$failure=1" "$work/generate-fixture" >"$work/error" 2>&1; then echo "generator admitted failed $failure" >&2; exit 1; fi + if env "$failure=1" "$work/generate-fixture" >"$work/error" 2>&1; then + echo "generator admitted failed $failure" >&2 + exit 1 + fi cmp "$work/observed-binding.json" "$production/scripts/canonical-tenant-source-bindings.json" done # Inconsistent direction one: bootstrap with an observed row. jq '.bindings[0].kind="published-bootstrap"' "$work/observed-binding.json" >"$production/scripts/canonical-tenant-source-bindings.json" -if "$work/generate-fixture" >"$work/error" 2>&1; then echo 'bootstrap borrowed observed authority' >&2; exit 1; fi +if "$work/generate-fixture" >"$work/error" 2>&1; then + echo 'bootstrap borrowed observed authority' >&2 + exit 1 +fi cp "$work/observed-binding.json" "$production/scripts/canonical-tenant-source-bindings.json" # Inconsistent direction two: binding without its real observed row. awk -F '\t' '$1!="business-site"' "$work/observed-identities.tsv" >"$production/scripts/publish-workflow-approved-revisions.identities.tsv" -if "$work/generate-fixture" >"$work/error" 2>&1; then echo 'binding invented missing observation' >&2; exit 1; fi +if "$work/generate-fixture" >"$work/error" 2>&1; then + echo 'binding invented missing observation' >&2 + exit 1 +fi cp "$work/observed-identities.tsv" "$production/scripts/publish-workflow-approved-revisions.identities.tsv" printf 'Canonical source bootstrap: atomic/idempotent admission, monotonic observed transition and refusal controls passed.\n' @@ -285,6 +324,7 @@ printf 'Canonical source bootstrap: atomic/idempotent admission, monotonic obser # source validation still passes; independent rendered validation must refuse. yq -i '.patches += [{"target":{"kind":"ResourceGraphDefinition","name":"tenant.kro.run"},"patch":"- op: replace\n path: /spec/resources/0/template/metadata/name\n value: unreviewed"}]' "$production/k8s/providers/hetzner/infrastructure/kustomization.yaml" if bash "$test_repo_root/scripts/guard-consumer-discovery-conservation.sh" "$production" >"$work/error" 2>&1; then - echo 'effective constructor changed behind admitted raw graph' >&2; exit 1 + echo 'effective constructor changed behind admitted raw graph' >&2 + exit 1 fi printf 'Canonical source bootstrap: effective rendered constructor changes are refused.\n' diff --git a/scripts/tests/test-canonical-source-upgrade.sh b/scripts/tests/test-canonical-source-upgrade.sh index 9605c0bd1b..fb84080088 100755 --- a/scripts/tests/test-canonical-source-upgrade.sh +++ b/scripts/tests/test-canonical-source-upgrade.sh @@ -74,19 +74,27 @@ kubectl() { case "$input_race" in approvals) awk -F '\t' '!($1=="business-site" && $4=="af042c540ce8db4d64add98c34b5c57691e0e812")' "$tree/scripts/publish-workflow-canonical-approvals.tsv" >"$upgrade_test_work/raced-approvals.tsv" - mv "$upgrade_test_work/raced-approvals.tsv" "$tree/scripts/publish-workflow-canonical-approvals.tsv" ;; + mv "$upgrade_test_work/raced-approvals.tsv" "$tree/scripts/publish-workflow-canonical-approvals.tsv" + ;; registry) jq '.bindings[0].publication.workflowRunAttempt=2' "$binding_file" >"$upgrade_test_work/raced.json" - mv "$upgrade_test_work/raced.json" "$binding_file" ;; + mv "$upgrade_test_work/raced.json" "$binding_file" + ;; tenant) yq -i '.spec.webApp=true' "$tree/$tenant_path" ;; constructor) yq -i '.spec.schema.group="unapproved.example"' "$tree/k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml" ;; - output) mkdir "$upgrade_test_work/candidate"; printf 'keep\n' >"$upgrade_test_work/candidate/keep" ;; + output) + mkdir "$upgrade_test_work/candidate" + printf 'keep\n' >"$upgrade_test_work/candidate/keep" + ;; esac else touch "$upgrade_test_work/read-once"; fi fi if [ "${observation_race:-0}" = 1 ]; then # A changed resourceVersion is enough to invalidate the preparation snapshot. - if [ -e "$upgrade_test_work/read-once" ]; then jq '.items[0].metadata.resourceVersion="changed"' "$upgrade_test_work/inventory.json"; return; fi + if [ -e "$upgrade_test_work/read-once" ]; then + jq '.items[0].metadata.resourceVersion="changed"' "$upgrade_test_work/inventory.json" + return + fi touch "$upgrade_test_work/read-once" fi cat "$upgrade_test_work/inventory.json" @@ -106,7 +114,8 @@ for value in '[.bindings[0].publication.publisherIdentity]' '[]'; do done approval_after_verification_race=1 if prepare >"$upgrade_test_work/result" 2>&1; then - echo 'approval change after verification was accepted' >&2;exit 1 + echo 'approval change after verification was accepted' >&2 + exit 1 fi test ! -e "$upgrade_test_work/candidate" cp "$upgrade_test_work/original-approvals.tsv" "$tree/scripts/publish-workflow-canonical-approvals.tsv" @@ -117,7 +126,11 @@ observed_business_tuple() { $1=="business-site" {n++; if($3!=tag || $4!=digest || $5!=publisher) bad=1} END {exit bad || n!=1}' "$file" } -prepare >"$upgrade_test_work/result" 2>"$upgrade_test_work/error" || { cat "$upgrade_test_work/error" >&2; echo 'real upgrade preparation failed' >&2; exit 1; } +prepare >"$upgrade_test_work/result" 2>"$upgrade_test_work/error" || { + cat "$upgrade_test_work/error" >&2 + echo 'real upgrade preparation failed' >&2 + exit 1 +} candidate_record="$upgrade_test_work/candidate/scripts/canonical-tenant-source-bindings.json" test -f "$candidate_record" test -f "$upgrade_test_work/candidate/$tenant_path" @@ -129,7 +142,10 @@ jq -e --arg digest "$next_digest" --argjson old "$old_record" ' (.previousObservation.uidSha256|test("^[0-9a-f]{64}$"))' "$candidate_record" >/dev/null test "$(yq '.spec.source.digest' "$upgrade_test_work/candidate/$tenant_path")" = "$next_digest" test "$(yq '.spec.webApp or .spec.externalDns or .spec.sops or .spec.source.reconcileApplication' "$upgrade_test_work/candidate/$tenant_path")" = false -if prepare >"$upgrade_test_work/result" 2>&1; then echo 'existing candidate directory overwritten' >&2; exit 1; fi +if prepare >"$upgrade_test_work/result" 2>&1; then + echo 'existing candidate directory overwritten' >&2 + exit 1 +fi cp -R "$upgrade_test_work/candidate" "$upgrade_test_work/good-candidate" rm -rf "$upgrade_test_work/candidate" prepare >"$upgrade_test_work/result" @@ -137,18 +153,27 @@ diff -r "$upgrade_test_work/good-candidate" "$upgrade_test_work/candidate" rm -rf "$upgrade_test_work/candidate" mkdir "$upgrade_test_work/redirect" ln -s "$upgrade_test_work/redirect" "$upgrade_test_work/candidate" -if prepare >"$upgrade_test_work/result" 2>&1; then echo 'candidate symlink followed' >&2; exit 1; fi +if prepare >"$upgrade_test_work/result" 2>&1; then + echo 'candidate symlink followed' >&2 + exit 1 +fi test -z "$(find "$upgrade_test_work/redirect" -mindepth 1 -print)" rm "$upgrade_test_work/candidate" observation_race=1 -if prepare >"$upgrade_test_work/result" 2>&1; then echo 'stale observation accepted' >&2; exit 1; fi +if prepare >"$upgrade_test_work/result" 2>&1; then + echo 'stale observation accepted' >&2 + exit 1 +fi test ! -e "$upgrade_test_work/candidate" observation_race=0 graph_path=k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml cp "$tree/$graph_path" "$upgrade_test_work/original-graph.yaml" for input_race in registry tenant constructor output approvals; do rm -f "$upgrade_test_work/read-once" - if prepare >"$upgrade_test_work/result" 2>&1; then echo "changed $input_race input accepted" >&2; exit 1; fi + if prepare >"$upgrade_test_work/result" 2>&1; then + echo "changed $input_race input accepted" >&2 + exit 1 + fi if [ "$input_race" = output ]; then test "$(find "$upgrade_test_work/candidate" -type f | wc -l | tr -d ' ')" = 1 grep -qx keep "$upgrade_test_work/candidate/keep" @@ -161,23 +186,29 @@ for input_race in registry tenant constructor output approvals; do done input_race='' for mutation in \ - '.items += .items' \ - '.metadata.continue="next-page"' \ - '.items[0].metadata.uid=""' \ - '.items[0].status.conditions[1].status="False"' \ - '.items[0].status.observedGeneration=1' \ - '.items[0].spec.verify.matchOIDCIdentity[0].subject=".*"' \ - '.items[0].spec.url="oci://ghcr.io/other/repository"' \ - '.items[0].spec.ref.digest="sha256:bad"'; do + '.items += .items' \ + '.metadata.continue="next-page"' \ + '.items[0].metadata.uid=""' \ + '.items[0].status.conditions[1].status="False"' \ + '.items[0].status.observedGeneration=1' \ + '.items[0].spec.verify.matchOIDCIdentity[0].subject=".*"' \ + '.items[0].spec.url="oci://ghcr.io/other/repository"' \ + '.items[0].spec.ref.digest="sha256:bad"'; do jq "$mutation" "$upgrade_test_work/original-inventory.json" >"$upgrade_test_work/inventory.json" - if prepare >"$upgrade_test_work/result" 2>&1; then echo "unsafe observed source accepted: $mutation" >&2; exit 1; fi + if prepare >"$upgrade_test_work/result" 2>&1; then + echo "unsafe observed source accepted: $mutation" >&2 + exit 1 + fi test ! -e "$upgrade_test_work/candidate" done cp "$upgrade_test_work/original-inventory.json" "$upgrade_test_work/inventory.json" cp "$upgrade_test_work/receipt.json" "$upgrade_test_work/good-receipt.json" for mutation in '.digest="sha256:bad"' '.sourceSHA=("9"*40)' '.gitTag="v3.0.0"' '.publisherIdentity="unapproved"' '.deploymentAuthorized=true' '.consumer="other"' '.event="push"' '.callerWorkflow=".github/workflows/other.yaml"'; do jq "$mutation" "$upgrade_test_work/good-receipt.json" >"$upgrade_test_work/receipt.json" - if prepare >"$upgrade_test_work/result" 2>&1; then echo "wrong candidate publication accepted: $mutation" >&2; exit 1; fi + if prepare >"$upgrade_test_work/result" 2>&1; then + echo "wrong candidate publication accepted: $mutation" >&2 + exit 1 + fi test ! -e "$upgrade_test_work/candidate" done cp "$upgrade_test_work/good-receipt.json" "$upgrade_test_work/receipt.json" @@ -202,9 +233,11 @@ default_observer() { } default_pin_resolver() { if [ "$1" = business-site ]; then - if [ -n "${TEST_MAIN_PUBLISHER:-}" ]; then printf '%s\n' "$TEST_MAIN_PUBLISHER" + if [ -n "${TEST_MAIN_PUBLISHER:-}" ]; then + printf '%s\n' "$TEST_MAIN_PUBLISHER" else printf '%s\n' "$record" | jq -r .publication.publisherIdentity; fi - elif [ "$1" = data-product-controller ]; then jq -r '.publications[]|select(.consumer=="data-product-controller")|.mainPinSHA' "$PUBLISH_BOOTSTRAP_FILE" + elif [ "$1" = data-product-controller ]; then + jq -r '.publications[]|select(.consumer=="data-product-controller")|.mainPinSHA' "$PUBLISH_BOOTSTRAP_FILE" else awk -F '\t' -v c="$1" '$1==c {print $6}' "$APPROVED_REVISIONS_FILE"; fi } canonical_publication_verify() { :; } @@ -213,7 +246,7 @@ test "$(jq -r '.bindings[0].kind' "$binding_file")" = published-upgrade test "$(awk -F '\t' '$1=="business-site" {n++} END {print n+0}' "$APPROVED_IDENTITIES_FILE")" = 0 jq --arg digest "$next_digest" '.items[0].spec.ref.digest=$digest | .items[0].status.artifact.revision=$digest | .items[0].metadata.generation=3 | .items[0].status.observedGeneration=3 | .items[0].status.conditions[].observedGeneration=3' \ - "$upgrade_test_work/original-inventory.json" >"$upgrade_test_work/inventory.json" + "$upgrade_test_work/original-inventory.json" >"$upgrade_test_work/inventory.json" cp "$upgrade_test_work/inventory.json" "$upgrade_test_work/replacement-ready.json" cp "$binding_file" "$upgrade_test_work/pending-binding.json" cp "$APPROVED_REVISIONS_FILE" "$upgrade_test_work/pending-revisions.tsv" @@ -224,14 +257,14 @@ cp "$PUBLISH_BOOTSTRAP_FILE" "$upgrade_test_work/pending-bootstrap.json" # on every refusal; reset fixture outputs so each pre-repair control is independent. unsafe_replacements=0 for mutation in \ - '.items[0].spec.verify.matchOIDCIdentity[0].issuer=".*"' \ - '.items[0].spec.verify.matchOIDCIdentity[0].subject=".*"' \ - '.items[0].spec.verify.provider="other"' \ - 'del(.items[0].spec.verify)' \ - '.items[0].spec.verify.matchOIDCIdentity += [{issuer:".*",subject:".*"}]' \ - '.items[0].spec.verify.matchOIDCIdentity[0].subject="unapproved-publisher"' \ - 'del(.items[0].metadata.uid)' \ - 'del(.items[0].metadata.resourceVersion)'; do + '.items[0].spec.verify.matchOIDCIdentity[0].issuer=".*"' \ + '.items[0].spec.verify.matchOIDCIdentity[0].subject=".*"' \ + '.items[0].spec.verify.provider="other"' \ + 'del(.items[0].spec.verify)' \ + '.items[0].spec.verify.matchOIDCIdentity += [{issuer:".*",subject:".*"}]' \ + '.items[0].spec.verify.matchOIDCIdentity[0].subject="unapproved-publisher"' \ + 'del(.items[0].metadata.uid)' \ + 'del(.items[0].metadata.resourceVersion)'; do jq "$mutation" "$upgrade_test_work/replacement-ready.json" >"$upgrade_test_work/inventory.json" if (main) >"$upgrade_test_work/replacement-result" 2>&1; then printf 'unsafe replacement accepted: %s\n' "$mutation" >&2 @@ -247,7 +280,10 @@ for mutation in \ cp "$upgrade_test_work/pending-identities.tsv" "$APPROVED_IDENTITIES_FILE" cp "$upgrade_test_work/pending-bootstrap.json" "$PUBLISH_BOOTSTRAP_FILE" done -[ "$unsafe_replacements" -eq 0 ] || { printf '%s unsafe replacement controls admitted\n' "$unsafe_replacements" >&2; exit 1; } +[ "$unsafe_replacements" -eq 0 ] || { + printf '%s unsafe replacement controls admitted\n' "$unsafe_replacements" >&2 + exit 1 +} cp "$upgrade_test_work/replacement-ready.json" "$upgrade_test_work/inventory.json" main >"$upgrade_test_work/generator-observed" test "$(jq -r '.bindings[0].kind' "$binding_file")" = published-binding @@ -262,7 +298,11 @@ cmp "$upgrade_test_work/applied-identities.tsv" "$APPROVED_IDENTITIES_FILE" # generator must refuse and preserve every generated output in this interval. cp "$APPROVED_REVISIONS_FILE" "$upgrade_test_work/gap-revisions.tsv" cp "$PUBLISH_BOOTSTRAP_FILE" "$upgrade_test_work/gap-bootstrap.json" -if (TEST_MAIN_PUBLISHER=devantler-tech/.github/.github/workflows/publish-app.yaml@af042c540ce8db4d64add98c34b5c57691e0e812; export TEST_MAIN_PUBLISHER; main) >"$upgrade_test_work/gap-result" 2>&1; then +if ( + TEST_MAIN_PUBLISHER=devantler-tech/.github/.github/workflows/publish-app.yaml@af042c540ce8db4d64add98c34b5c57691e0e812 + export TEST_MAIN_PUBLISHER + main +) >"$upgrade_test_work/gap-result" 2>&1; then echo 'main pin alone changed the current publication identity' >&2 exit 1 fi @@ -293,7 +333,7 @@ cp "$binding_file" "$upgrade_test_work/second-pending-binding.json" cp "$APPROVED_IDENTITIES_FILE" "$upgrade_test_work/second-pending-identities.tsv" jq --arg digest "$next_digest" '.items[0].spec.ref.digest=$digest | .items[0].status.artifact.revision=$digest | .items[0].metadata.generation=4 | .items[0].status.observedGeneration=4 | .items[0].status.conditions[].observedGeneration=4' \ - "$upgrade_test_work/original-inventory.json" >"$upgrade_test_work/inventory.json" + "$upgrade_test_work/original-inventory.json" >"$upgrade_test_work/inventory.json" if (main) >"$upgrade_test_work/generator-old-matcher" 2>&1; then echo 'old approved matcher accepted for the proposed publisher' >&2 exit 1 @@ -304,7 +344,7 @@ cmp "$upgrade_test_work/second-pending-identities.tsv" "$APPROVED_IDENTITIES_FIL jq --arg digest "$next_digest" '.items[0].spec.ref.digest=$digest | .items[0].status.artifact.revision=$digest | .items[0].spec.verify.matchOIDCIdentity[0].subject="^https://github\\.com/devantler-tech/\\.github/\\.github/workflows/publish-app\\.yaml@af042c540ce8db4d64add98c34b5c57691e0e812$" | .items[0].metadata.generation=4 | .items[0].status.observedGeneration=4 | .items[0].status.conditions[].observedGeneration=4' \ - "$upgrade_test_work/original-inventory.json" >"$upgrade_test_work/inventory.json" + "$upgrade_test_work/original-inventory.json" >"$upgrade_test_work/inventory.json" main >"$upgrade_test_work/generator-selected-observed" test "$(jq -r '.bindings[0].kind' "$binding_file")" = published-binding jq -e '.bindings[0].publication.publisherIdentity=="devantler-tech/.github/.github/workflows/publish-app.yaml@af042c540ce8db4d64add98c34b5c57691e0e812"' "$binding_file" >/dev/null diff --git a/scripts/tests/test-check-flux-orphaned-objects.sh b/scripts/tests/test-check-flux-orphaned-objects.sh index ec868612eb..478a0e5ebb 100755 --- a/scripts/tests/test-check-flux-orphaned-objects.sh +++ b/scripts/tests/test-check-flux-orphaned-objects.sh @@ -683,10 +683,16 @@ regression_explicit_adoption() { if [[ -n "${FLUX_ORPHANS_TEST_REGRESSION:-}" ]]; then case "${FLUX_ORPHANS_TEST_REGRESSION}" in - metadata) regression_metadata_storage; regression_auxiliary_metadata_storage ;; + metadata) + regression_metadata_storage + regression_auxiliary_metadata_storage + ;; attribution) regression_baseline_attribution ;; settle) regression_always_settle ;; - *) echo 'unknown regression selector' >&2; exit 2 ;; + *) + echo 'unknown regression selector' >&2 + exit 2 + ;; esac echo "check-flux-orphaned-objects: selected regression passed (${run_count} CLI fixture runs)" exit 0 diff --git a/scripts/tests/test-cilium-hubble-baseline-context.sh b/scripts/tests/test-cilium-hubble-baseline-context.sh index 4eb3f41a7f..52aa583080 100755 --- a/scripts/tests/test-cilium-hubble-baseline-context.sh +++ b/scripts/tests/test-cilium-hubble-baseline-context.sh @@ -9,7 +9,10 @@ scratch="$(mktemp -d)" trap 'rm -rf "${scratch}"' EXIT release_dir="${root_dir}/k8s/bases/infrastructure/controllers/cilium" -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} for tool in helm yq jq; do command -v "${tool}" >/dev/null || fail "${tool} is required" done diff --git a/scripts/tests/test-cleanup-orphan-generated-vpas.sh b/scripts/tests/test-cleanup-orphan-generated-vpas.sh index 7722a71a8f..ac3f9e3780 100755 --- a/scripts/tests/test-cleanup-orphan-generated-vpas.sh +++ b/scripts/tests/test-cleanup-orphan-generated-vpas.sh @@ -87,18 +87,36 @@ export VPA_CLEANUP_CONFIRM=cleanup-orphan-generated-vpas run_case() { local scenario="$1" mode="$2" expected="$3" deleted="$4" rc=0 export CASE="$work/$scenario-$mode" SCENARIO="$scenario" - mkdir -p "$CASE"; cp "$work/base.json" "$CASE/vpa.json" + mkdir -p "$CASE" + cp "$work/base.json" "$CASE/vpa.json" case "$scenario" in - wrong-label) jq '.metadata.labels["generate.kyverno.io/trigger-namespace"]="other"' "$CASE/vpa.json" >"$CASE/new"; mv "$CASE/new" "$CASE/vpa.json" ;; - wrong-target) jq '.spec.targetRef.name="other"' "$CASE/vpa.json" >"$CASE/new"; mv "$CASE/new" "$CASE/vpa.json" ;; - invalid-uid) jq '.metadata.labels["generate.kyverno.io/trigger-uid"]=""' "$CASE/vpa.json" >"$CASE/new"; mv "$CASE/new" "$CASE/vpa.json" ;; - owned) jq '.metadata.ownerReferences=[{"apiVersion":"apps/v1","kind":"Deployment","name":"old","uid":"00000000-0000-0000-0000-000000000002"}]' "$CASE/vpa.json" >"$CASE/new"; mv "$CASE/new" "$CASE/vpa.json" ;; + wrong-label) + jq '.metadata.labels["generate.kyverno.io/trigger-namespace"]="other"' "$CASE/vpa.json" >"$CASE/new" + mv "$CASE/new" "$CASE/vpa.json" + ;; + wrong-target) + jq '.spec.targetRef.name="other"' "$CASE/vpa.json" >"$CASE/new" + mv "$CASE/new" "$CASE/vpa.json" + ;; + invalid-uid) + jq '.metadata.labels["generate.kyverno.io/trigger-uid"]=""' "$CASE/vpa.json" >"$CASE/new" + mv "$CASE/new" "$CASE/vpa.json" + ;; + owned) + jq '.metadata.ownerReferences=[{"apiVersion":"apps/v1","kind":"Deployment","name":"old","uid":"00000000-0000-0000-0000-000000000002"}]' "$CASE/vpa.json" >"$CASE/new" + mv "$CASE/new" "$CASE/vpa.json" + ;; esac bash "$root/scripts/cleanup-orphan-generated-vpas.sh" "$mode" >"$CASE/output" 2>&1 || rc=$? - [[ "$rc" == "$expected" ]] || { cat "$CASE/output"; echo "FAIL: $scenario/$mode exit $rc expected $expected"; exit 1; } + [[ "$rc" == "$expected" ]] || { + cat "$CASE/output" + echo "FAIL: $scenario/$mode exit $rc expected $expected" + exit 1 + } if [[ "$deleted" == yes ]]; then [[ -f "$CASE/deleted" ]]; else [[ ! -f "$CASE/deleted" ]]; fi if grep -Eq 'apps-ns|old-deployment|00000000-|Forbidden|https?://' "$CASE/output"; then - echo "FAIL: $scenario exposed private input"; exit 1 + echo "FAIL: $scenario exposed private input" + exit 1 fi echo "ok: $scenario/$mode" } @@ -113,7 +131,10 @@ run_case delete-failed apply 2 no run_case leftover apply 1 yes export GITHUB_REF=refs/heads/unreviewed run_case unreviewed apply 1 no -[[ ! -f "$CASE/calls" ]] || { echo 'FAIL: unreviewed dispatch reached the API'; exit 1; } +[[ ! -f "$CASE/calls" ]] || { + echo 'FAIL: unreviewed dispatch reached the API' + exit 1 +} echo 'OK: generated VPA cleanup preserves live and changed objects' # Execute the actual workflow guard, including every branch before checkout or @@ -139,7 +160,8 @@ for invalid in branch repository event attempt confirm mode; do esac bash "$work/guard.sh" ) >/dev/null 2>&1; then - echo "FAIL: the workflow accepted $invalid"; exit 1 + echo "FAIL: the workflow accepted $invalid" + exit 1 fi done echo 'OK: the main-only workflow guard refuses every invalid dispatch control' diff --git a/scripts/tests/test-coroot-application-health-reconciler.sh b/scripts/tests/test-coroot-application-health-reconciler.sh index 7723ed60ef..6f726ca7e2 100755 --- a/scripts/tests/test-coroot-application-health-reconciler.sh +++ b/scripts/tests/test-coroot-application-health-reconciler.sh @@ -703,33 +703,33 @@ observe_query_slots() { fi [[ "$pid" =~ ^[1-9][0-9]*$ ]] || return 1 case "$event" in - start) - [[ ! -e "${dir}/live-query-$pid" && -p "${dir}/query-$pid.gate" ]] || return 1 - : >"${dir}/live-query-$pid" - active=$((active + 1)) - ((peak >= active)) || peak=$active - decision=go - ((active <= limit)) || decision=reject - printf '%s' "$decision" >"${dir}/query-$pid.decision" - pending="$pending $pid" - printf 'start pid=%s active=%s decision=%s\n' "$pid" "$active" "$decision" >>"${dir}/query-events" - if ((overlap == 0 || active >= overlap)); then - for client in $pending; do - exec 8<>"${dir}/query-$client.gate" - printf '%s\n' "$(cat "${dir}/query-$client.decision")" >&8 - exec 8>&- - done - pending='' - overlap=0 - fi - ;; - end) - [[ -f "${dir}/live-query-$pid" && "$active" -gt 0 ]] || return 1 - rm "${dir}/live-query-$pid" - active=$((active - 1)) - printf 'end pid=%s active=%s\n' "$pid" "$active" >>"${dir}/query-events" - ;; - *) return 1 ;; + start) + [[ ! -e "${dir}/live-query-$pid" && -p "${dir}/query-$pid.gate" ]] || return 1 + : >"${dir}/live-query-$pid" + active=$((active + 1)) + ((peak >= active)) || peak=$active + decision=go + ((active <= limit)) || decision=reject + printf '%s' "$decision" >"${dir}/query-$pid.decision" + pending="$pending $pid" + printf 'start pid=%s active=%s decision=%s\n' "$pid" "$active" "$decision" >>"${dir}/query-events" + if ((overlap == 0 || active >= overlap)); then + for client in $pending; do + exec 8<>"${dir}/query-$client.gate" + printf '%s\n' "$(cat "${dir}/query-$client.decision")" >&8 + exec 8>&- + done + pending='' + overlap=0 + fi + ;; + end) + [[ -f "${dir}/live-query-$pid" && "$active" -gt 0 ]] || return 1 + rm "${dir}/live-query-$pid" + active=$((active - 1)) + printf 'end pid=%s active=%s\n' "$pid" "$active" >>"${dir}/query-events" + ;; + *) return 1 ;; esac done return 1 diff --git a/scripts/tests/test-coroot-dns-resolution.sh b/scripts/tests/test-coroot-dns-resolution.sh index 6dd57ec63e..8f7a2bb18b 100755 --- a/scripts/tests/test-coroot-dns-resolution.sh +++ b/scripts/tests/test-coroot-dns-resolution.sh @@ -8,7 +8,10 @@ readonly root_dir policy scratch trap 'rm -rf "${scratch}"' EXIT for tool in docker go kyverno yq; do - command -v "${tool}" >/dev/null || { printf '%s is required\n' "${tool}" >&2; exit 1; } + command -v "${tool}" >/dev/null || { + printf '%s is required\n' "${tool}" >&2 + exit 1 + } done # Exercise admission, then feed its effective option to both real resolvers. diff --git a/scripts/tests/test-coroot-reservation-refresh.sh b/scripts/tests/test-coroot-reservation-refresh.sh index 8dd4f20776..fb96436259 100755 --- a/scripts/tests/test-coroot-reservation-refresh.sh +++ b/scripts/tests/test-coroot-reservation-refresh.sh @@ -8,7 +8,10 @@ readonly scratch trap 'rm -rf "${scratch}"' EXIT for tool in kubectl yq; do - command -v "${tool}" >/dev/null || { printf '%s is required\n' "${tool}" >&2; exit 1; } + command -v "${tool}" >/dev/null || { + printf '%s is required\n' "${tool}" >&2 + exit 1 + } done # Inspect the delivered custom resource, not just the overlay's patch. The diff --git a/scripts/tests/test-cronjob-failure-alert.sh b/scripts/tests/test-cronjob-failure-alert.sh index e69f2a43d9..58cc05d5c6 100755 --- a/scripts/tests/test-cronjob-failure-alert.sh +++ b/scripts/tests/test-cronjob-failure-alert.sh @@ -22,7 +22,10 @@ readonly documented_manifest='bases/components/coroot-cronjob-failure-alert/cron readonly real_webhook='https://hooks.test.invalid/delivery-target' readonly placeholder_webhook='https://example.invalid/no-slack-configured' -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} pass() { printf 'ok: %s\n' "$1"; } for tool in yq jq; do @@ -245,7 +248,11 @@ expect_error() { # dir label stderr-needle # The shape #2915 is about: the newest two runs both failed. dir="$(setup_scenario consecutive)" -{ job run-4 Failed 60; job run-3 Failed 960; job run-2 Complete 1860; } | job_list >"${dir}/jobs.body" +{ + job run-4 Failed 60 + job run-3 Failed 960 + job run-2 Complete 1860 +} | job_list >"${dir}/jobs.body" expect_alert "$dir" 'two consecutive failed runs alert, naming the target and both runs' \ 'umami/umami-provision-tenants' 'the last 2 runs all failed' run-4 run-3 BackoffLimitExceeded grep -Fq 'run-2' "${dir}/tmp/payload.json" && fail 'a run outside the failure window was named' @@ -254,38 +261,65 @@ dir="$(setup_scenario kubescape-single-failure)" job cleanup-failed Failed 60 kubescape-hostdata-cleanup | job_list >"${dir}/jobs.body" WATCH_OVERRIDE='kubescape/kubescape-hostdata-cleanup:1:93600' \ expect_alert "$dir" 'the daily Kubescape cleanup alerts on its first failed run' \ - 'kubescape/kubescape-hostdata-cleanup' 'the last 1 runs all failed' cleanup-failed + 'kubescape/kubescape-hostdata-cleanup' 'the last 1 runs all failed' cleanup-failed # Ordering is by schedule time, not list order: the same runs listed oldest # first must give the same verdict. dir="$(setup_scenario list-order)" -{ job run-2 Complete 1860; job run-3 Failed 960; job run-4 Failed 60; } | job_list >"${dir}/jobs.body" +{ + job run-2 Complete 1860 + job run-3 Failed 960 + job run-4 Failed 60 +} | job_list >"${dir}/jobs.body" expect_alert "$dir" 'the verdict does not depend on list order' run-4 run-3 # Control arm: the loop absorbs one failure by design (the weekly credential # rotation). A single failure followed by success stays quiet ... dir="$(setup_scenario absorbed)" -{ job run-3 Complete 60; job run-2 Failed 960; job run-1 Failed 1860; } | job_list >"${dir}/jobs.body" +{ + job run-3 Complete 60 + job run-2 Failed 960 + job run-1 Failed 1860 +} | job_list >"${dir}/jobs.body" expect_quiet "$dir" 'a success after failures clears the alert' 'healthy' # ... and so does a lone newest failure. dir="$(setup_scenario single)" -{ job run-2 Failed 60; job run-1 Complete 960; } | job_list >"${dir}/jobs.body" +{ + job run-2 Failed 60 + job run-1 Complete 960 +} | job_list >"${dir}/jobs.body" expect_quiet "$dir" 'a single failed run is absorbed, not alerted' 'healthy' # A still-running Job is not a finished run and must not break the streak. dir="$(setup_scenario running)" -{ job run-3 Running 30; job run-2 Failed 960; job run-1 Failed 1860; } | job_list >"${dir}/jobs.body" +{ + job run-3 Running 30 + job run-2 Failed 960 + job run-1 Failed 1860 +} | job_list >"${dir}/jobs.body" expect_alert "$dir" 'an in-flight run neither counts nor hides the streak' run-2 run-1 # Another CronJob's failures in the same namespace are not this loop's. They are # the NEWEST runs here, so an owner filter that stopped matching would alert. dir="$(setup_scenario foreign)" -{ job other-2 Failed 60 umami-other; job other-1 Failed 960 umami-other; job run-1 Complete 1800; } | job_list >"${dir}/jobs.body" +{ + job other-2 Failed 60 umami-other + job other-1 Failed 960 umami-other + job run-1 Complete 1800 +} | job_list >"${dir}/jobs.body" expect_quiet "$dir" "another CronJob's failures are ignored" 'healthy' # Threshold boundary: with threshold 3, two failures are quiet and three alert. dir="$(setup_scenario threshold)" -{ job run-3 Failed 60; job run-2 Failed 960; job run-1 Complete 1860; } | job_list >"${dir}/jobs.body" +{ + job run-3 Failed 60 + job run-2 Failed 960 + job run-1 Complete 1860 +} | job_list >"${dir}/jobs.body" THRESHOLD_OVERRIDE=3 expect_quiet "$dir" 'fewer failures than the threshold stay quiet' 'healthy' -{ job run-3 Failed 60; job run-2 Failed 960; job run-1 Failed 1860; } | job_list >"${dir}/jobs.body" +{ + job run-3 Failed 60 + job run-2 Failed 960 + job run-1 Failed 1860 +} | job_list >"${dir}/jobs.body" THRESHOLD_OVERRIDE=3 expect_alert "$dir" 'exactly the threshold of failures alerts' 'the last 3 runs all failed' # Silence is not health: no run finishing within STALE_SECONDS alerts. @@ -336,7 +370,10 @@ WATCH_OVERRIDE='umami/umami-provision-tenants:4:3600' \ # Delivery: the placeholder host never delivers; a failed delivery fails the Job. dir="$(setup_scenario placeholder "$placeholder_webhook")" -{ job run-2 Failed 60; job run-1 Failed 960; } | job_list >"${dir}/jobs.body" +{ + job run-2 Failed 60 + job run-1 Failed 960 +} | job_list >"${dir}/jobs.body" for shell in "${shells[@]}"; do run_scenario "$dir" "$shell" || fail "placeholder [${shell}] exited non-zero" [ ! -f "${dir}/delivered" ] || fail "placeholder [${shell}] delivered" @@ -344,7 +381,10 @@ for shell in "${shells[@]}"; do done pass 'the placeholder webhook is never delivered to' dir="$(setup_scenario delivery-fails)" -{ job run-2 Failed 60; job run-1 Failed 960; } | job_list >"${dir}/jobs.body" +{ + job run-2 Failed 60 + job run-1 Failed 960 +} | job_list >"${dir}/jobs.body" for shell in "${shells[@]}"; do if STUB_DELIVERY_EXIT=22 run_scenario "$dir" "$shell"; then fail "failed delivery [${shell}] reported success"; fi done diff --git a/scripts/tests/test-crossplane-aws-family-version-parity.sh b/scripts/tests/test-crossplane-aws-family-version-parity.sh index d37cfdf859..bf631def81 100755 --- a/scripts/tests/test-crossplane-aws-family-version-parity.sh +++ b/scripts/tests/test-crossplane-aws-family-version-parity.sh @@ -59,10 +59,14 @@ provider() { # --- Self-test: each verdict on a fixture, so a check that stopped matching # cannot pass the real render vacuously. -{ provider upbound-provider-family-aws "${family}:v2.8.1" - provider provider-aws-iam "${member_prefix}iam:v2.8.1"; } >"${workdir}/equal.yaml" -{ provider upbound-provider-family-aws "${family}:v2.8.1" - provider provider-aws-iam "${member_prefix}iam:v2.8.0"; } >"${workdir}/skewed.yaml" +{ + provider upbound-provider-family-aws "${family}:v2.8.1" + provider provider-aws-iam "${member_prefix}iam:v2.8.1" +} >"${workdir}/equal.yaml" +{ + provider upbound-provider-family-aws "${family}:v2.8.1" + provider provider-aws-iam "${member_prefix}iam:v2.8.0" +} >"${workdir}/skewed.yaml" provider provider-aws-iam "${member_prefix}iam:v2.8.1" >"${workdir}/no-family.yaml" provider upbound-provider-family-aws "${family}:v2.8.1" >"${workdir}/no-member.yaml" diff --git a/scripts/tests/test-crossplane-sync-alerter.sh b/scripts/tests/test-crossplane-sync-alerter.sh index 3d3e6a0ba5..db13cad3da 100755 --- a/scripts/tests/test-crossplane-sync-alerter.sh +++ b/scripts/tests/test-crossplane-sync-alerter.sh @@ -10,7 +10,10 @@ config="${root_dir}/k8s/bases/infrastructure/coroot/components/crossplane-sync-e work_dir="$(mktemp -d)" trap 'chmod -R u+w "${work_dir}"; rm -rf "${work_dir}"' EXIT -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} pod_path='.spec.jobTemplate.spec.template.spec' scratch_volume="$(yq -r "${pod_path}.containers[] | select(.name == \"alerter\") | .volumeMounts[] | select(.mountPath == \"/tmp\") | .name" "${manifest}")" diff --git a/scripts/tests/test-descheduler-availability.sh b/scripts/tests/test-descheduler-availability.sh index 88bb0a8ce4..0b09c42a49 100755 --- a/scripts/tests/test-descheduler-availability.sh +++ b/scripts/tests/test-descheduler-availability.sh @@ -10,7 +10,10 @@ scratch="$(mktemp -d)" readonly root_dir release repository scratch trap 'rm -rf "${scratch}"' EXIT -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} for tool in helm jq kubectl kyverno yq; do command -v "${tool}" >/dev/null || fail "${tool} is required" done diff --git a/scripts/tests/test-dex-theme-mount.sh b/scripts/tests/test-dex-theme-mount.sh index be842f4329..495e2754b9 100755 --- a/scripts/tests/test-dex-theme-mount.sh +++ b/scripts/tests/test-dex-theme-mount.sh @@ -62,15 +62,24 @@ check_theme() { local rendered="$1" local count values theme dir mount_path volume sub_path cm_name items key size logo logo_file - count="$(read_all "[${dex_release}] | length" "${rendered}")" || { printf '%s\n' "${count}"; return 1; } + count="$(read_all "[${dex_release}] | length" "${rendered}")" || { + printf '%s\n' "${count}" + return 1 + } if [[ "${count}" != 1 ]]; then printf 'renders %s dex HelmReleases in namespace dex instead of one\n' "${count:-no}" return 1 fi values="${workdir}/dex-values.yaml" - read_yaml "${dex_release} | .spec.values" "${rendered}" >"${values}" || { cat "${values}"; return 1; } + read_yaml "${dex_release} | .spec.values" "${rendered}" >"${values}" || { + cat "${values}" + return 1 + } - theme="$(read_yaml '.config.frontend.theme // ""' "${values}")" || { printf '%s\n' "${theme}"; return 1; } + theme="$(read_yaml '.config.frontend.theme // ""' "${values}")" || { + printf '%s\n' "${theme}" + return 1 + } if [[ -z "${theme}" ]]; then printf 'the dex HelmRelease names no config.frontend.theme, so Dex shows its built-in light theme\n' return 1 @@ -80,7 +89,10 @@ check_theme() { return 1 fi - dir="$(read_yaml '.config.frontend.dir // ""' "${values}")" || { printf '%s\n' "${dir}"; return 1; } + dir="$(read_yaml '.config.frontend.dir // ""' "${values}")" || { + printf '%s\n' "${dir}" + return 1 + } if [[ "${dir}" != /* ]]; then printf 'the dex HelmRelease names no absolute config.frontend.dir, so Dex serves its built-in files and never sees a mounted theme\n' return 1 @@ -88,46 +100,70 @@ check_theme() { mount_path="${dir%/}/themes/${theme}" count="$(MOUNT="${mount_path}" read_yaml '[.volumeMounts[]? | select((.mountPath | sub("/$"; "")) == strenv(MOUNT))] | length' "${values}")" || - { printf '%s\n' "${count}"; return 1; } + { + printf '%s\n' "${count}" + return 1 + } if [[ "${count}" != 1 ]]; then printf "the dex HelmRelease mounts %s volumes at %s, where Dex looks for theme '%s'; it needs exactly one\n" "${count}" "${mount_path}" "${theme}" return 1 fi volume="$(MOUNT="${mount_path}" read_yaml '.volumeMounts[] | select((.mountPath | sub("/$"; "")) == strenv(MOUNT)) | .name // ""' "${values}")" || - { printf '%s\n' "${volume}"; return 1; } + { + printf '%s\n' "${volume}" + return 1 + } sub_path="$(MOUNT="${mount_path}" read_yaml '.volumeMounts[] | select((.mountPath | sub("/$"; "")) == strenv(MOUNT)) | (.subPath // "") + (.subPathExpr // "")' "${values}")" || - { printf '%s\n' "${sub_path}"; return 1; } + { + printf '%s\n' "${sub_path}" + return 1 + } if [[ -n "${sub_path}" ]]; then printf 'the theme is mounted at %s with a subPath; mount the whole ConfigMap so the theme is a directory\n' "${mount_path}" return 1 fi - count="$(VOL="${volume}" read_yaml '[.volumes[]? | select(.name == strenv(VOL))] | length' "${values}")" || { printf '%s\n' "${count}"; return 1; } + count="$(VOL="${volume}" read_yaml '[.volumes[]? | select(.name == strenv(VOL))] | length' "${values}")" || { + printf '%s\n' "${count}" + return 1 + } if [[ -z "${volume}" || "${count}" != 1 ]]; then printf "the volume '%s' mounted at %s is defined %s times in the dex HelmRelease instead of once\n" "${volume}" "${mount_path}" "${count}" return 1 fi cm_name="$(VOL="${volume}" read_yaml '.volumes[] | select(.name == strenv(VOL)) | .configMap.name // ""' "${values}")" || - { printf '%s\n' "${cm_name}"; return 1; } + { + printf '%s\n' "${cm_name}" + return 1 + } if [[ -z "${cm_name}" ]]; then printf "the volume '%s' mounted at %s is not a ConfigMap volume\n" "${volume}" "${mount_path}" return 1 fi items="$(VOL="${volume}" read_yaml '.volumes[] | select(.name == strenv(VOL)) | .configMap.items // [] | length' "${values}")" || - { printf '%s\n' "${items}"; return 1; } + { + printf '%s\n' "${items}" + return 1 + } if [[ "${items}" != 0 ]]; then printf "the volume '%s' selects ConfigMap keys with items; mount every key, the way this check reads them\n" "${volume}" return 1 fi local -r configmap="select(.kind == \"ConfigMap\" and .metadata.name == strenv(CM) and .metadata.namespace == \"dex\")" - count="$(CM="${cm_name}" read_all "[${configmap}] | length" "${rendered}")" || { printf '%s\n' "${count}"; return 1; } + count="$(CM="${cm_name}" read_all "[${configmap}] | length" "${rendered}")" || { + printf '%s\n' "${count}" + return 1 + } if [[ "${count}" != 1 ]]; then printf "the ConfigMap '%s' that the theme is mounted from is rendered %s times in namespace dex in this layer instead of once\n" "${cm_name}" "${count}" return 1 fi - logo="$(read_yaml '.config.frontend.logoURL // ""' "${values}")" || { printf '%s\n' "${logo}"; return 1; } + logo="$(read_yaml '.config.frontend.logoURL // ""' "${values}")" || { + printf '%s\n' "${logo}" + return 1 + } if [[ -z "${logo}" ]]; then logo_file=logo.png # Dex's default logoURL is theme/logo.png. elif [[ "${logo}" =~ ^theme/([A-Za-z0-9._-]+)$ ]]; then @@ -139,14 +175,20 @@ check_theme() { # styles.css is text, so it must be in data; the images may be in either map. size="$(CM="${cm_name}" read_yaml "${configmap} | .data[\"styles.css\"] // \"\" | length" "${rendered}")" || - { printf '%s\n' "${size}"; return 1; } + { + printf '%s\n' "${size}" + return 1 + } if [[ -z "${size}" || "${size}" == 0 ]]; then printf "the ConfigMap '%s' has no styles.css in data, so the pages render unstyled\n" "${cm_name}" return 1 fi for key in favicon.png "${logo_file}"; do size="$(CM="${cm_name}" KEY="${key}" read_yaml "${configmap} | (.data[strenv(KEY)] // .binaryData[strenv(KEY)] // \"\") | length" "${rendered}")" || - { printf '%s\n' "${size}"; return 1; } + { + printf '%s\n' "${size}" + return 1 + } if [[ -z "${size}" || "${size}" == 0 ]]; then printf "the ConfigMap '%s' has no %s, which the pages link to as theme/%s\n" "${cm_name}" "${key}" "${key}" return 1 diff --git a/scripts/tests/test-dr-longhorn-disk-preflight.sh b/scripts/tests/test-dr-longhorn-disk-preflight.sh index eb13708c10..717343327a 100755 --- a/scripts/tests/test-dr-longhorn-disk-preflight.sh +++ b/scripts/tests/test-dr-longhorn-disk-preflight.sh @@ -11,7 +11,10 @@ filter="$(awk ' } END { if (found != 1 || in_filter) exit 1 } ' "${runbook}")" -[[ -n "${filter}" ]] || { echo 'Missing runbook disk query' >&2; exit 1; } +[[ -n "${filter}" ]] || { + echo 'Missing runbook disk query' >&2 + exit 1 +} check() { local label="$1" input="$2" expected="$3" actual diff --git a/scripts/tests/test-fetch-cilium-deny-candidate.sh b/scripts/tests/test-fetch-cilium-deny-candidate.sh index 2deccc8931..2669c3d025 100755 --- a/scripts/tests/test-fetch-cilium-deny-candidate.sh +++ b/scripts/tests/test-fetch-cilium-deny-candidate.sh @@ -32,13 +32,27 @@ for name in success author fork moved closed read_error after_read reread_error; PATH="${work}/bin:${PATH}" TEST_ROOT="$root" FAKE_STATE="${work}/${name}" FAKE_CASE="$name" \ RUNNER_TEMP="${work}/${name}/temp" CANDIDATE_PR=4381 CANDIDATE_HEAD=01e40ecf795a3eaf6c0e30ba461a7253afb36462 \ bash "${root}/.github/scripts/fetch-cilium-deny-candidate.sh" >"${work}/${name}/output" 2>&1 || result=$? - expected=1; [[ "$name" != success ]] || expected=0 - if [[ "$result" != "$expected" ]]; then echo "FAIL $name: expected $expected, got $result"; cat "${work}/${name}/output"; failures=$((failures + 1)); fi + expected=1 + [[ "$name" != success ]] || expected=0 + if [[ "$result" != "$expected" ]]; then + echo "FAIL $name: expected $expected, got $result" + cat "${work}/${name}/output" + failures=$((failures + 1)) + fi if [[ "$name" == success ]]; then receipt="${work}/${name}/temp/cilium-deny-candidate/candidate.json" - if ! jq -e '.head == "01e40ecf795a3eaf6c0e30ba461a7253afb36462" and .author == "devantler" and (.generator_sha256|length)==64 and (.flux_copy_sha256|length)==64' "$receipt" >/dev/null; then echo 'FAIL candidate receipt'; failures=$((failures + 1)); fi - elif [[ "$name" != after_read && "$name" != reread_error && -f "${work}/${name}/requests" ]] && grep -Fq '/contents/' "${work}/${name}/requests"; then echo "FAIL $name: fetched before trust/head gate"; failures=$((failures + 1)); fi - if [[ "$name" != success && -f "${work}/${name}/temp/cilium-deny-candidate/candidate.json" ]]; then echo "FAIL $name: rejected candidate got a receipt"; failures=$((failures + 1)); fi + if ! jq -e '.head == "01e40ecf795a3eaf6c0e30ba461a7253afb36462" and .author == "devantler" and (.generator_sha256|length)==64 and (.flux_copy_sha256|length)==64' "$receipt" >/dev/null; then + echo 'FAIL candidate receipt' + failures=$((failures + 1)) + fi + elif [[ "$name" != after_read && "$name" != reread_error && -f "${work}/${name}/requests" ]] && grep -Fq '/contents/' "${work}/${name}/requests"; then + echo "FAIL $name: fetched before trust/head gate" + failures=$((failures + 1)) + fi + if [[ "$name" != success && -f "${work}/${name}/temp/cilium-deny-candidate/candidate.json" ]]; then + echo "FAIL $name: rejected candidate got a receipt" + failures=$((failures + 1)) + fi done [[ "$failures" == 0 ]] || exit 1 echo 'PASS: only exact same-repo trusted current heads reach fixed candidate data reads' diff --git a/scripts/tests/test-frozen-legacy-publisher-release.sh b/scripts/tests/test-frozen-legacy-publisher-release.sh index 7b4f57229d..5c37a88820 100755 --- a/scripts/tests/test-frozen-legacy-publisher-release.sh +++ b/scripts/tests/test-frozen-legacy-publisher-release.sh @@ -13,7 +13,7 @@ readonly header=$'repository\tworkflow\ttag\tcommit\tworkflow_blob\tobserved_on' # Old code gets plausible answers, but the test still fails if it makes ANY # request. This distinguishes removing the dependency from masking its failure. -cat > "$work/bin/gh" <<'STUB' +cat >"$work/bin/gh" <<'STUB' #!/usr/bin/env bash printf '%s\n' "$*" >> "$FROZEN_TEST_LOG" case "$*" in @@ -27,21 +27,21 @@ STUB chmod +x "$work/bin/gh" snapshot="$work/valid.tsv" -printf '%s\n' "$header" > "$snapshot" -printf 'devantler-tech/actions\tpublish-app\tv13.10.12\t%s\t%s\t2026-10-20\n' "$frozen" "$app_blob" >> "$snapshot" -printf 'devantler-tech/actions\tpublish-manifests\tv13.10.12\t%s\t%s\t2026-10-20\n' "$frozen" "$manifests_blob" >> "$snapshot" +printf '%s\n' "$header" >"$snapshot" +printf 'devantler-tech/actions\tpublish-app\tv13.10.12\t%s\t%s\t2026-10-20\n' "$frozen" "$app_blob" >>"$snapshot" +printf 'devantler-tech/actions\tpublish-manifests\tv13.10.12\t%s\t%s\t2026-10-20\n' "$frozen" "$manifests_blob" >>"$snapshot" passed=0 failed=0 run_case() { local name="$1" workflow="$2" record="$3" want="$4" output rc=0 - : > "$work/requests.log" + : >"$work/requests.log" output="$(env PATH="$work/bin:$PATH" FROZEN_TEST_LOG="$work/requests.log" \ FROZEN_LEGACY_RELEASE_FILE="$record" bash -c \ "source \"\$1\"; default_release_resolver \"\$2\"" bash "$sut" "$workflow" 2>"$work/error.log")" || rc=$? - if [[ -s "$work/requests.log" || ( "$want" == accept && ( "$rc" != 0 || "$output" != "$frozen" ) ) || \ - ( "$want" == reject && ( "$rc" == 0 || -n "$output" ) ) ]]; then - printf 'FAIL: %s (exit=%s, network_requests=%s)\n' "$name" "$rc" "$(wc -l < "$work/requests.log" | tr -d ' ')" + if [[ -s "$work/requests.log" || ("$want" == accept && ("$rc" != 0 || "$output" != "$frozen")) || + ("$want" == reject && ("$rc" == 0 || -n "$output")) ]]; then + printf 'FAIL: %s (exit=%s, network_requests=%s)\n' "$name" "$rc" "$(wc -l <"$work/requests.log" | tr -d ' ')" failed=$((failed + 1)) else passed=$((passed + 1)) @@ -55,7 +55,7 @@ run_case 'missing snapshot is refused' publish-app "$work/missing.tsv" reject mutate() { local name="$1" expression="$2" - sed "$expression" "$snapshot" > "$work/mutant.tsv" + sed "$expression" "$snapshot" >"$work/mutant.tsv" run_case "$name" publish-app "$work/mutant.tsv" reject } mutate 'wrong header' '1s/repository/repo/' @@ -79,10 +79,10 @@ mutate 'extra field' '2s/$/\textra/' mutate 'missing final field' '2s/\t2026-10-20$//' cp "$snapshot" "$work/mutant.tsv" -sed -n '2p' "$snapshot" >> "$work/mutant.tsv" +sed -n '2p' "$snapshot" >>"$work/mutant.tsv" run_case 'duplicate row' publish-app "$work/mutant.tsv" reject cp "$snapshot" "$work/mutant.tsv" -printf '\n' >> "$work/mutant.tsv" +printf '\n' >>"$work/mutant.tsv" run_case 'blank extra row' publish-app "$work/mutant.tsv" reject ln -s "$snapshot" "$work/link.tsv" run_case 'symlink snapshot' publish-app "$work/link.tsv" reject @@ -112,16 +112,16 @@ done source "$root_dir/scripts/tests/lib/legacy-consumer-fixture.sh" legacy_fixture="$work/legacy-consumers" build_legacy_consumer_fixture "$root_dir" "$legacy_fixture" -cat > "$work/observe" <<'STUB' +cat >"$work/observe" <<'STUB' #!/usr/bin/env bash printf '1.2.3\tsha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\tea4df9004d4ab43b6c9af5b0c5f55712f7211328\n' STUB -cat > "$work/pin" <<'STUB' +cat >"$work/pin" <<'STUB' #!/usr/bin/env bash printf '%s\n' ea4df9004d4ab43b6c9af5b0c5f55712f7211328 STUB chmod +x "$work/observe" "$work/pin" -printf '{"schemaVersion":1,"publications":[]}\n' > "$work/bootstrap.json" +printf '{"schemaVersion":1,"publications":[]}\n' >"$work/bootstrap.json" generate() { env PATH="$work/bin:$PATH" FROZEN_TEST_LOG="$work/requests.log" \ PUBLISH_CONSUMER_ROOT="$legacy_fixture" \ @@ -129,10 +129,10 @@ generate() { FROZEN_LEGACY_RELEASE_FILE="$1" APPROVED_REVISION_RELEASE_RESOLVER='' \ APPROVED_REVISION_OBSERVER="$work/observe" APPROVED_REVISION_PIN_RESOLVER="$work/pin" \ PUBLISH_BOOTSTRAP_FILE="$work/bootstrap.json" APPROVED_REVISIONS_FILE="$work/generated.tsv" \ - bash "$sut" > "$work/generation.log" 2>&1 + bash "$sut" >"$work/generation.log" 2>&1 } -: > "$work/requests.log" -if generate '' && [[ ! -s "$work/requests.log" ]] && \ +: >"$work/requests.log" +if generate '' && [[ ! -s "$work/requests.log" ]] && awk -F'\t' -v candidate="$frozen" 'NR>1 { if (NF!=8 || $7!=candidate || $1=="business-site") bad=1; rows++ } END {exit bad || rows!=5}' "$work/generated.tsv"; then passed=$((passed + 1)) else @@ -142,9 +142,9 @@ fi if [[ -f "$work/generated.tsv" && -f "$work/generated.identities.tsv" ]]; then cp "$work/generated.tsv" "$work/generated.before" cp "$work/generated.identities.tsv" "$work/identities.before" - : > "$work/requests.log" - if generate "$work/missing.tsv" || [[ -s "$work/requests.log" ]] || \ - ! cmp -s "$work/generated.tsv" "$work/generated.before" || \ + : >"$work/requests.log" + if generate "$work/missing.tsv" || [[ -s "$work/requests.log" ]] || + ! cmp -s "$work/generated.tsv" "$work/generated.before" || ! cmp -s "$work/generated.identities.tsv" "$work/identities.before"; then printf 'FAIL: invalid frozen evidence must refuse without changing either output\n' failed=$((failed + 1)) diff --git a/scripts/tests/test-github-bootstrap-isolation.sh b/scripts/tests/test-github-bootstrap-isolation.sh index d3b3844e69..f87b4b9992 100755 --- a/scripts/tests/test-github-bootstrap-isolation.sh +++ b/scripts/tests/test-github-bootstrap-isolation.sh @@ -28,7 +28,11 @@ done for fixture in empty duplicate malformed; do case "${fixture}" in empty) printf 'kind: ConfigMap\n' >"${work}/unknown.yaml" ;; - duplicate) cat "${work}/github.yaml" >"${work}/unknown.yaml"; printf '\n---\n' >>"${work}/unknown.yaml"; cat "${work}/github.yaml" >>"${work}/unknown.yaml" ;; + duplicate) + cat "${work}/github.yaml" >"${work}/unknown.yaml" + printf '\n---\n' >>"${work}/unknown.yaml" + cat "${work}/github.yaml" >>"${work}/unknown.yaml" + ;; malformed) printf 'kind: [\n' >"${work}/unknown.yaml" ;; esac status=0 diff --git a/scripts/tests/test-github-team-identity-admission.sh b/scripts/tests/test-github-team-identity-admission.sh index 74a9df7e04..2e4685a39a 100755 --- a/scripts/tests/test-github-team-identity-admission.sh +++ b/scripts/tests/test-github-team-identity-admission.sh @@ -61,8 +61,8 @@ team() { } control() { jq -cn --arg label "$1" --arg policy "$policy_name" --arg actor "$2" --argjson object "$3" --arg expected "$4" \ - '{label: $label, policy: $policy, object: $object, oldObject: null, actor: $actor, operation: "CREATE", expected: $expected}' \ - | jq -c 'del(.oldObject)' >>"$controls" + '{label: $label, policy: $policy, object: $object, oldObject: null, actor: $actor, operation: "CREATE", expected: $expected}' | + jq -c 'del(.oldObject)' >>"$controls" } foreign_team="$(team github-config 99999999)" provider_prefix='system:serviceaccount:crossplane-system:provider-upjet-github-' diff --git a/scripts/tests/test-guard-cilium-clusterwide-default-deny.sh b/scripts/tests/test-guard-cilium-clusterwide-default-deny.sh index 66bc60fac8..cd234d7eca 100755 --- a/scripts/tests/test-guard-cilium-clusterwide-default-deny.sh +++ b/scripts/tests/test-guard-cilium-clusterwide-default-deny.sh @@ -48,7 +48,8 @@ fixture() { } # RED: the pre-#3407 shape — an egressDeny, no egress allow, no enableDefaultDeny. -pre_3407="$(fixture pre-3407 <<'EOF' +pre_3407="$( + fixture pre-3407 <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: @@ -66,7 +67,8 @@ EOF expect 'pre-#3407 policy is refused' 1 'deny-workload-instance-metadata-egress spec: egressDeny rules with no egress allow rules' "$pre_3407" expect 'the refusal says what to do' 1 'egress: false' "$pre_3407" -explicit_false="$(fixture explicit-false <<'EOF' +explicit_false="$( + fixture explicit-false <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: subtract-one} @@ -81,7 +83,8 @@ expect 'explicit enableDefaultDeny false passes' 0 '' "$explicit_false" # A null is pruned by the API server, so Cilium applies its default (true): only a # real boolean states the intent. -null_value="$(fixture null-value <<'EOF' +null_value="$( + fixture null-value <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: null-intent} @@ -94,7 +97,8 @@ EOF )" expect 'a null enableDefaultDeny direction is refused' 1 'null-intent spec: egressDeny' "$null_value" -string_value="$(fixture string-value <<'EOF' +string_value="$( + fixture string-value <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: string-intent} @@ -107,7 +111,8 @@ EOF )" expect 'a non-boolean enableDefaultDeny direction is refused' 1 'string-intent spec: egressDeny' "$string_value" -explicit_true="$(fixture explicit-true <<'EOF' +explicit_true="$( + fixture explicit-true <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: lock-down} @@ -120,7 +125,8 @@ EOF )" expect 'explicit enableDefaultDeny true passes' 0 '' "$explicit_true" -with_allow="$(fixture with-allow <<'EOF' +with_allow="$( + fixture with-allow <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: allow-list} @@ -134,7 +140,8 @@ EOF )" expect 'deny alongside allow rules is ordinary allow-list semantics' 0 '' "$with_allow" -wrong_direction="$(fixture wrong-direction <<'EOF' +wrong_direction="$( + fixture wrong-direction <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: wrong-direction} @@ -147,7 +154,8 @@ EOF )" expect 'enableDefaultDeny for the OTHER direction does not count' 1 'no enableDefaultDeny.egress' "$wrong_direction" -ingress_only="$(fixture ingress-only <<'EOF' +ingress_only="$( + fixture ingress-only <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: ingress-deny} @@ -159,7 +167,8 @@ EOF )" expect 'deny-only ingress is refused and named' 1 'ingressDeny rules with no ingress allow rules' "$ingress_only" -multi_spec="$(fixture multi-spec <<'EOF' +multi_spec="$( + fixture multi-spec <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: multi} @@ -175,7 +184,8 @@ EOF )" expect 'one unguarded spec among guarded ones is refused by index' 1 'CiliumClusterwideNetworkPolicy/multi specs[1]: egressDeny' "$multi_spec" -both_fields="$(fixture both-fields <<'EOF' +both_fields="$( + fixture both-fields <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: both} @@ -192,7 +202,8 @@ EOF )" expect 'a deny-only spec beside compliant specs is still checked (Cilium applies both)' 1 'CiliumClusterwideNetworkPolicy/both spec: egressDeny' "$both_fields" -multi_doc="$(fixture multi-doc <<'EOF' +multi_doc="$( + fixture multi-doc <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: good} @@ -214,7 +225,8 @@ EOF expect 'a bad policy in the second document of a stream is found' 1 'CiliumClusterwideNetworkPolicy/bad spec:' "$multi_doc" expect 'every rendered file is checked, not just the first' 1 'CiliumClusterwideNetworkPolicy/bad' "$explicit_false" "$multi_doc" -namespaced="$(fixture namespaced <<'EOF' +namespaced="$( + fixture namespaced <<'EOF' apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: {name: namespaced, namespace: demo} @@ -227,7 +239,8 @@ EOF expect 'no clusterwide policy at all is cannot-check' 2 'nothing was verified' "$namespaced" expect 'namespaced policies are out of scope beside a checked clusterwide one' 0 '' "$explicit_false" "$namespaced" -other_group="$(fixture other-group <<'EOF' +other_group="$( + fixture other-group <<'EOF' apiVersion: example.com/v1 kind: CiliumClusterwideNetworkPolicy metadata: {name: not-cilium} @@ -238,7 +251,8 @@ EOF )" expect 'a same-named kind from another API group is not Cilium' 0 '' "$explicit_false" "$other_group" -no_api="$(fixture no-api <<'EOF' +no_api="$( + fixture no-api <<'EOF' kind: CiliumClusterwideNetworkPolicy metadata: {name: no-api} spec: @@ -249,7 +263,8 @@ EOF )" expect 'a missing apiVersion is cannot-check, never skipped' 2 'CiliumClusterwideNetworkPolicy/no-api' "$explicit_false" "$no_api" -bare_group="$(fixture bare-group <<'EOF' +bare_group="$( + fixture bare-group <<'EOF' apiVersion: cilium.io/ kind: CiliumClusterwideNetworkPolicy metadata: {name: bare-group} @@ -259,7 +274,8 @@ EOF )" expect 'cilium.io with no version is cannot-check' 2 'CiliumClusterwideNetworkPolicy/bare-group' "$explicit_false" "$bare_group" -no_spec="$(fixture no-spec <<'EOF' +no_spec="$( + fixture no-spec <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: no-spec} @@ -267,7 +283,8 @@ EOF )" expect 'a policy with no spec is cannot-check' 2 'no spec or specs' "$no_spec" -broken="$(fixture broken <<'EOF' +broken="$( + fixture broken <<'EOF' apiVersion: cilium.io/v2 kind: CiliumClusterwideNetworkPolicy metadata: {name: broken diff --git a/scripts/tests/test-guard-consumer-discovery-conservation.sh b/scripts/tests/test-guard-consumer-discovery-conservation.sh index 50ba068fbf..60f7f7dc26 100755 --- a/scripts/tests/test-guard-consumer-discovery-conservation.sh +++ b/scripts/tests/test-guard-consumer-discovery-conservation.sh @@ -436,11 +436,13 @@ regression_published_closure() { case "$boundary" in uppercase-json) reference='selected.JSON' - yq -o=json '.' "$WORK/selected.yaml" >"$root/k8s/providers/prod/infrastructure/$reference" ;; + yq -o=json '.' "$WORK/selected.yaml" >"$root/k8s/providers/prod/infrastructure/$reference" + ;; ignored-yml) reference='.selected.YmL' cp "$WORK/selected.yaml" "$root/k8s/providers/prod/infrastructure/$reference" - printf '.selected.YmL\n' >"$root/k8s/providers/prod/infrastructure/.gitignore" ;; + printf '.selected.YmL\n' >"$root/k8s/providers/prod/infrastructure/.gitignore" + ;; *) cp "$WORK/selected.yaml" "$root/k8s/providers/prod/infrastructure/$reference" ;; esac printf ' - %s\n' "$reference" >>"$root/k8s/providers/prod/infrastructure/kustomization.yaml" @@ -553,7 +555,7 @@ exec "$REAL_YQ" "$@" SH chmod +x "$WORK/closure-successful-partial-$boundary-bin/yq" REAL_YQ="$(command -v yq)" SUCCESSFUL_PARTIAL="$boundary" \ - PATH="$WORK/closure-successful-partial-$boundary-bin:$PATH" \ + PATH="$WORK/closure-successful-partial-$boundary-bin:$PATH" \ closure_expect_no_render "successful $boundary path extraction cannot attest completeness" "$root" done @@ -568,11 +570,14 @@ SH patches | patchesJson6902 | replacements) printf '%s:\n - path: %s\n' "$field" "$reference" >>"$config" ;; patchesStrategicMerge | configurations | crds | transformers | generators) printf '%s:\n - %s\n' "$field" "$reference" >>"$config" ;; configMap-files | secret-files) - printf '%sGenerator:\n - name: loader\n files:\n - key=%s\n' "${field%-files}" "$reference" >>"$config" ;; + printf '%sGenerator:\n - name: loader\n files:\n - key=%s\n' "${field%-files}" "$reference" >>"$config" + ;; configMap-envs | secret-envs) - printf '%sGenerator:\n - name: loader\n envs:\n - %s\n' "${field%-envs}" "$reference" >>"$config" ;; + printf '%sGenerator:\n - name: loader\n envs:\n - %s\n' "${field%-envs}" "$reference" >>"$config" + ;; configMap-env | secret-env) - printf '%sGenerator:\n - name: loader\n env: %s\n' "${field%-env}" "$reference" >>"$config" ;; + printf '%sGenerator:\n - name: loader\n env: %s\n' "${field%-env}" "$reference" >>"$config" + ;; openapi) printf 'openapi:\n path: %s\n' "$reference" >>"$config" ;; esac closure_expect_no_render "$field $boundary inputs must refuse before rendering" "$root" @@ -593,13 +598,16 @@ SH printf 'patches:\n - path: loader.yaml\n' >>"$config" else printf 'patchesStrategicMerge:\n - loader.yaml\n' >>"$config" - fi ;; + fi + ;; patchesJson6902) printf '[{"op":"replace","path":"/data/value","value":"unchanged"}]\n' >"${config%/*}/loader.json" - printf 'patchesJson6902:\n - target:\n version: v1\n kind: ConfigMap\n name: input\n path: loader.json\n' >>"$config" ;; + printf 'patchesJson6902:\n - target:\n version: v1\n kind: ConfigMap\n name: input\n path: loader.json\n' >>"$config" + ;; configurations) printf 'namePrefix:\n - path: metadata/name\n' >"${config%/*}/loader.yaml" - printf 'configurations:\n - loader.yaml\n' >>"$config" ;; + printf 'configurations:\n - loader.yaml\n' >>"$config" + ;; crds | openapi) if [ "$field" = openapi ]; then printf '{"swagger":"2.0","info":{"title":"fixture","version":"v1"},"paths":{},"definitions":{}}\n' >"${config%/*}/loader.json" @@ -607,27 +615,34 @@ SH else printf '{"fixture":{"type":"object"}}\n' >"${config%/*}/loader.json" printf 'crds:\n - loader.json\n' >>"$config" - fi ;; + fi + ;; transformers) printf 'apiVersion: builtin\nkind: AnnotationsTransformer\nmetadata:\n name: loader\nannotations:\n fixture: local\nfieldSpecs:\n - path: metadata/annotations\n create: true\n' >"${config%/*}/loader.yaml" - printf 'transformers:\n - loader.yaml\n' >>"$config" ;; + printf 'transformers:\n - loader.yaml\n' >>"$config" + ;; generators) mkdir "${config%/*}/nested" printf 'apiVersion: builtin\nkind: ConfigMapGenerator\nmetadata:\n name: loader\nfiles:\n - settings.yaml\n' >"${config%/*}/nested/loader.yaml" printf 'setting=value\n' >"${config%/*}/settings.yaml" - printf 'generators:\n - nested/loader.yaml\n' >>"$config" ;; + printf 'generators:\n - nested/loader.yaml\n' >>"$config" + ;; configMap-files | secret-files) printf 'setting=value\n' >"${config%/*}/settings.yaml" - printf '%sGenerator:\n - name: loader\n files:\n - key=settings.yaml\n' "${field%-files}" >>"$config" ;; + printf '%sGenerator:\n - name: loader\n files:\n - key=settings.yaml\n' "${field%-files}" >>"$config" + ;; configMap-envs | secret-envs) printf 'setting=value\n' >"${config%/*}/settings.yaml" - printf '%sGenerator:\n - name: loader\n envs:\n - settings.yaml\n' "${field%-envs}" >>"$config" ;; + printf '%sGenerator:\n - name: loader\n envs:\n - settings.yaml\n' "${field%-envs}" >>"$config" + ;; configMap-env | secret-env) printf 'setting=value\n' >"${config%/*}/settings.yaml" - printf '%sGenerator:\n - name: loader\n env: settings.yaml\n' "${field%-env}" >>"$config" ;; + printf '%sGenerator:\n - name: loader\n env: settings.yaml\n' "${field%-env}" >>"$config" + ;; replacements) printf 'source:\n kind: ConfigMap\n name: input\n fieldPath: data.value\ntargets:\n - select:\n kind: ConfigMap\n name: input\n fieldPaths:\n - data.value\n' >"${config%/*}/loader.yaml" - printf 'replacements:\n - path: loader.yaml\n' >>"$config" ;; + printf 'replacements:\n - path: loader.yaml\n' >>"$config" + ;; esac expect_pass "$field retains valid selected local-file inputs" "$root" '2 consumer(s)' done @@ -695,10 +710,12 @@ YAML *-envs) printf 'envs:\n - https://example.test/unavailable.yaml\n' >>"${config%/*}/loader.yaml" ;; *-env) printf 'env: https://example.test/unavailable.yaml\n' >>"${config%/*}/loader.yaml" ;; *) printf 'files:\n - key=https://example.test/unavailable.yaml\n' >>"${config%/*}/loader.yaml" ;; - esac ;; + esac + ;; patch-transformer*) reference='transformers' - printf 'apiVersion: builtin\nkind: PatchTransformer\nmetadata:\n name: loader\npath: https://example.test/unavailable.yaml\n' >"${config%/*}/loader.yaml" ;; + printf 'apiVersion: builtin\nkind: PatchTransformer\nmetadata:\n name: loader\npath: https://example.test/unavailable.yaml\n' >"${config%/*}/loader.yaml" + ;; esac if [[ "$field" = *-inline ]]; then LOADER="$(cat "${config%/*}/loader.yaml")" FIELD="$reference" yq -i '.[strenv(FIELD)] = [strenv(LOADER)]' "$config" @@ -778,13 +795,16 @@ YAML case "$boundary" in remote) yq -i '.transformers[0] |= sub("replacement.yaml", "https://example.test/unavailable.yaml")' "$config" - closure_expect_no_render 'inline builtin replacement paths refuse before rendering' "$root" ;; + closure_expect_no_render 'inline builtin replacement paths refuse before rendering' "$root" + ;; local) printf 'source:\n kind: ConfigMap\n name: replacement-input\n fieldPath: data.value\ntargets:\n - select:\n kind: ConfigMap\n name: replacement-input\n fieldPaths:\n - data.value\n' >"${config%/*}/replacement.yaml" - expect_pass 'inline builtin replacement paths retain selected local files' "$root" '2 consumer(s)' ;; + expect_pass 'inline builtin replacement paths retain selected local files' "$root" '2 consumer(s)' + ;; malformed) yq -i '.transformers[0] = "apiVersion: builtin\nkind: ReplacementTransformer\nmetadata:\n name: replacements\nreplacements:\n path: missing.yaml\n"' "$config" - closure_expect_no_render 'malformed inline replacements cannot authorize a loader' "$root" ;; + closure_expect_no_render 'malformed inline replacements cannot authorize a loader' "$root" + ;; esac done } @@ -912,9 +932,18 @@ regression_consumer_identities() { for field in name namespace url; do root="$(fixture "identity-patched-$field")" case "$field" in - name) target='/metadata/name'; value='beta-renamed' ;; - namespace) target='/metadata/namespace'; value='beta-other' ;; - url) target='/spec/url'; value='oci://ghcr.io/devantler-tech/beta/manifests/' ;; + name) + target='/metadata/name' + value='beta-renamed' + ;; + namespace) + target='/metadata/namespace' + value='beta-other' + ;; + url) + target='/spec/url' + value='oci://ghcr.io/devantler-tech/beta/manifests/' + ;; esac cat >>"$root/k8s/providers/prod/apps/kustomization.yaml" <"$root/k8s/providers/prod/apps/mutation.yaml" <<'YAML' + cel) + cat >"$root/k8s/providers/prod/apps/mutation.yaml" <<'YAML' apiVersion: policies.kyverno.io/v1 kind: MutatingPolicy metadata: @@ -1199,7 +1229,10 @@ YAML extra-json-patch-field) yq -i '.spec.mutations[0].jsonPatch.extra = "x"' "$root/k8s/providers/prod/apps/cel-mutation.yaml" ;; esac cmp -s "$WORK/reviewed-cel-mutation/k8s/providers/prod/apps/cel-mutation.yaml" "$root/k8s/providers/prod/apps/cel-mutation.yaml" && - { printf 'FAIL: the %s variant did not change the reviewed MutatingPolicy\n' "$target" >&2; failures=$((failures + 1)); } + { + printf 'FAIL: the %s variant did not change the reviewed MutatingPolicy\n' "$target" >&2 + failures=$((failures + 1)) + } printf ' - cel-mutation.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_refusal "a $target MutatingPolicy is outside the reviewed shape" "$root" 'unevaluated CEL admission mutation' 'consumers are UNKNOWN' done @@ -1307,7 +1340,10 @@ YAML root="$(fixture reviewed-cel-generation-one-line)" yq '.spec.generate[0].expression |= sub("\n *"; "")' "$WORK/reviewed-cel-generation/k8s/providers/prod/apps/cel-generation.yaml" >"$root/k8s/providers/prod/apps/cel-generation.yaml" cmp -s "$WORK/reviewed-cel-generation/k8s/providers/prod/apps/cel-generation.yaml" "$root/k8s/providers/prod/apps/cel-generation.yaml" && - { printf 'FAIL: the one-line variant did not change the reviewed GeneratingPolicy\n' >&2; failures=$((failures + 1)); } + { + printf 'FAIL: the one-line variant did not change the reviewed GeneratingPolicy\n' >&2 + failures=$((failures + 1)) + } printf ' - cel-generation.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_pass 'the reviewed auto-vpa GeneratingPolicy on one line does not alter consumers' "$root" '2 consumer(s)' # #5125: a NotIn exclusion of named namespaces passes too; a label selector @@ -1332,7 +1368,10 @@ YAML extra-expression-key) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions[0].extra = "x"' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; esac cmp -s "$WORK/reviewed-cel-generation-namespace-exclusion/k8s/providers/prod/apps/cel-generation.yaml" "$root/k8s/providers/prod/apps/cel-generation.yaml" && - { printf 'FAIL: the %s variant did not change the namespace-excluded GeneratingPolicy\n' "$target" >&2; failures=$((failures + 1)); } + { + printf 'FAIL: the %s variant did not change the namespace-excluded GeneratingPolicy\n' "$target" >&2 + failures=$((failures + 1)) + } printf ' - cel-generation.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_refusal "a $target namespace selector is outside the reviewed shape" "$root" 'unevaluated CEL admission mutation' 'consumers are UNKNOWN' done @@ -1369,7 +1408,10 @@ YAML any-version) yq -i '.spec.matchConstraints.resourceRules[0].apiVersions = ["*"]' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; esac cmp -s "$WORK/reviewed-cel-generation/k8s/providers/prod/apps/cel-generation.yaml" "$root/k8s/providers/prod/apps/cel-generation.yaml" && - { printf 'FAIL: the %s variant did not change the reviewed GeneratingPolicy\n' "$target" >&2; failures=$((failures + 1)); } + { + printf 'FAIL: the %s variant did not change the reviewed GeneratingPolicy\n' "$target" >&2 + failures=$((failures + 1)) + } printf ' - cel-generation.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_refusal "a $target GeneratingPolicy is outside the reviewed shape" "$root" 'unevaluated CEL admission mutation' 'consumers are UNKNOWN' done @@ -1564,7 +1606,8 @@ spec: YAML case "$placement" in steps) yq -i '.spec.steps = [{"name":"instances","resources":.spec.resources}] | del(.spec.resources)' "$root/k8s/providers/prod/apps/carrier.yaml" ;; - generation) cat >"$root/k8s/providers/prod/apps/carrier.yaml" <<'YAML' + generation) + cat >"$root/k8s/providers/prod/apps/carrier.yaml" <<'YAML' apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: @@ -1710,7 +1753,10 @@ YAML missing-resource) yq -i 'del(.webhooks[0].rules[0].resources)' "$root/k8s/providers/prod/apps/webhook.yaml" ;; missing-operation) yq -i 'del(.webhooks[0].rules[0].operations)' "$root/k8s/providers/prod/apps/webhook.yaml" ;; substituted-operation) yq -i '.webhooks[0].rules[0].operations = ["${SOURCE_OPERATION}"]' "$root/k8s/providers/prod/apps/webhook.yaml" ;; - substituted-key) yq -i '.webhooks[0]["${RULES_FIELD}"] = .webhooks[0].rules | del(.webhooks[0].rules)' "$root/k8s/providers/prod/apps/webhook.yaml"; want='mapping key' ;; + substituted-key) + yq -i '.webhooks[0]["${RULES_FIELD}"] = .webhooks[0].rules | del(.webhooks[0].rules)' "$root/k8s/providers/prod/apps/webhook.yaml" + want='mapping key' + ;; esac printf ' - webhook.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_refusal "a $target native mutator cannot attest a static consumer" "$root" "$want" 'UNKNOWN' @@ -1807,8 +1853,14 @@ YAML case "$placement" in match) TARGET_KIND="$target" yq -i '.spec.rules[0].match.any[0].resources.kinds = [strenv(TARGET_KIND)]' "$root/k8s/providers/prod/apps/policy.yaml" ;; targets) TARGET_KIND="$target" yq -i '.spec.rules[0].mutate.targets = [{"apiVersion":"example.test/v1","kind":strenv(TARGET_KIND)}]' "$root/k8s/providers/prod/apps/policy.yaml" ;; - generate) TARGET_KIND="$target" yq -i 'del(.spec.rules[0].mutate) | .spec.rules[0].generate = {"apiVersion":"example.test/v1","kind":strenv(TARGET_KIND),"name":"carrier","data":{"spec":{}}}' "$root/k8s/providers/prod/apps/policy.yaml"; want='consumer generation' ;; - clone) TARGET_KIND="$target" yq -i 'del(.spec.rules[0].mutate) | .spec.rules[0].generate.cloneList = {"kinds":["example.test/v1/" + strenv(TARGET_KIND)],"namespace":"existing"}' "$root/k8s/providers/prod/apps/policy.yaml"; want='consumer generation' ;; + generate) + TARGET_KIND="$target" yq -i 'del(.spec.rules[0].mutate) | .spec.rules[0].generate = {"apiVersion":"example.test/v1","kind":strenv(TARGET_KIND),"name":"carrier","data":{"spec":{}}}' "$root/k8s/providers/prod/apps/policy.yaml" + want='consumer generation' + ;; + clone) + TARGET_KIND="$target" yq -i 'del(.spec.rules[0].mutate) | .spec.rules[0].generate.cloneList = {"kinds":["example.test/v1/" + strenv(TARGET_KIND)],"namespace":"existing"}' "$root/k8s/providers/prod/apps/policy.yaml" + want='consumer generation' + ;; esac printf ' - policy.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_refusal "$placement cannot hide a runtime $target carrier" "$root" "$want" 'UNKNOWN' @@ -1841,7 +1893,8 @@ YAML # shellcheck disable=SC2016 # Flux substitutes this mapping key after the static build. yq -i '.spec.resources[0].spec["${TEMPLATE_FIELD}"] = .spec.resources[0].spec.resourcesTemplate | del(.spec.resources[0].spec.resourcesTemplate)' "$root/k8s/providers/prod/apps/carrier.yaml" yq -i '.spec.postBuild.substitute.TEMPLATE_FIELD = "resourcesTemplate"' "$root/k8s/clusters/prod/flux-kustomizations.yaml" - want='mapping key' ;; + want='mapping key' + ;; esac printf ' - carrier.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_refusal "a nested ResourceSet $placement string cannot escape the census" "$root" "$want" 'UNKNOWN' @@ -1875,24 +1928,50 @@ spec: YAML want='consumer-producing schema' case "$target" in - custom-group) yq -i '.spec.schema.group = "tenants.example.test"' "$root/k8s/bases/infrastructure/tenant-rgd/resource-graph-definition.yaml"; yq -i '.spec.rules[0].generate.cloneList.kinds = ["tenants.example.test/v1alpha1/Tenant"]' "$root/k8s/providers/prod/apps/generate.yaml" ;; + custom-group) + yq -i '.spec.schema.group = "tenants.example.test"' "$root/k8s/bases/infrastructure/tenant-rgd/resource-graph-definition.yaml" + yq -i '.spec.rules[0].generate.cloneList.kinds = ["tenants.example.test/v1alpha1/Tenant"]' "$root/k8s/providers/prod/apps/generate.yaml" + ;; kind-only) yq -i '.spec.rules[0].generate.cloneList.kinds = ["Tenant"]' "$root/k8s/providers/prod/apps/generate.yaml" ;; version-kind) yq -i '.spec.rules[0].generate.cloneList.kinds = ["v1alpha1/Tenant"]' "$root/k8s/providers/prod/apps/generate.yaml" ;; - foreach|foreach-source) yq -i '.spec.rules[0].generate = {"foreach":[{"list":"request.object.metadata.labels","cloneList":.spec.rules[0].generate.cloneList}]}' "$root/k8s/providers/prod/apps/generate.yaml" - if [ "$target" = foreach-source ]; then yq -i '.spec.rules[0].generate.foreach[0].cloneList.kinds = ["source.toolkit.fluxcd.io/v1/OCIRepository"]' "$root/k8s/providers/prod/apps/generate.yaml"; want='consumer generation'; fi ;; + foreach | foreach-source) + yq -i '.spec.rules[0].generate = {"foreach":[{"list":"request.object.metadata.labels","cloneList":.spec.rules[0].generate.cloneList}]}' "$root/k8s/providers/prod/apps/generate.yaml" + if [ "$target" = foreach-source ]; then + yq -i '.spec.rules[0].generate.foreach[0].cloneList.kinds = ["source.toolkit.fluxcd.io/v1/OCIRepository"]' "$root/k8s/providers/prod/apps/generate.yaml" + want='consumer generation' + fi + ;; foreign-group) yq -i '.spec.rules[0].generate.cloneList.kinds = ["other.example.test/v1alpha1/Tenant"]' "$root/k8s/providers/prod/apps/generate.yaml" ;; foreign-version) yq -i '.spec.rules[0].generate.cloneList.kinds = ["kro.run/v9/Tenant"]' "$root/k8s/providers/prod/apps/generate.yaml" ;; unrelated-foreach) yq -i '.spec.rules[0].generate = {"foreach":[{"list":"request.object.metadata.labels","apiVersion":"v1","kind":"ConfigMap","name":"ordinary","data":{"data":{"ordinary":"value"}}}]}' "$root/k8s/providers/prod/apps/generate.yaml" ;; - malformed-match) yq -i '.spec.rules[0].generate = null | .spec.rules[0].mutate.patchStrategicMerge.metadata.labels.ordinary = "value" | .spec.rules[0].match.any[0].resources.kinds = {"bad":"Pod"} | del(.spec.rules[0].generate)' "$root/k8s/providers/prod/apps/generate.yaml"; want='admission mutation' ;; - malformed-clone) yq -i '.spec.rules[0].generate.cloneList.kinds = {"bad":"v1/ConfigMap"}' "$root/k8s/providers/prod/apps/generate.yaml"; want='consumer generation' ;; - empty-foreach) yq -i '.spec.rules[0].generate = {"foreach":[]}' "$root/k8s/providers/prod/apps/generate.yaml"; want='consumer generation' ;; - unknown-foreach) yq -i '.spec.rules[0].generate = {"foreach":[{"list":"request.object.metadata.labels","data":{}}]}' "$root/k8s/providers/prod/apps/generate.yaml"; want='consumer generation' ;; - nested-foreach) yq -i '.spec.rules[0].generate = {"foreach":[{"foreach":[{"kind":"ConfigMap"}]}]}' "$root/k8s/providers/prod/apps/generate.yaml"; want='consumer generation' ;; - cel) yq -i '.apiVersion = "policies.kyverno.io/v1" | .kind = "GeneratingPolicy" | .spec = {"generation":[{"expression":"generator.Apply(\"v1\", \"configmaps\", \"default\", [])"}]}' "$root/k8s/providers/prod/apps/generate.yaml"; want='unevaluated CEL' ;; + malformed-match) + yq -i '.spec.rules[0].generate = null | .spec.rules[0].mutate.patchStrategicMerge.metadata.labels.ordinary = "value" | .spec.rules[0].match.any[0].resources.kinds = {"bad":"Pod"} | del(.spec.rules[0].generate)' "$root/k8s/providers/prod/apps/generate.yaml" + want='admission mutation' + ;; + malformed-clone) + yq -i '.spec.rules[0].generate.cloneList.kinds = {"bad":"v1/ConfigMap"}' "$root/k8s/providers/prod/apps/generate.yaml" + want='consumer generation' + ;; + empty-foreach) + yq -i '.spec.rules[0].generate = {"foreach":[]}' "$root/k8s/providers/prod/apps/generate.yaml" + want='consumer generation' + ;; + unknown-foreach) + yq -i '.spec.rules[0].generate = {"foreach":[{"list":"request.object.metadata.labels","data":{}}]}' "$root/k8s/providers/prod/apps/generate.yaml" + want='consumer generation' + ;; + nested-foreach) + yq -i '.spec.rules[0].generate = {"foreach":[{"foreach":[{"kind":"ConfigMap"}]}]}' "$root/k8s/providers/prod/apps/generate.yaml" + want='consumer generation' + ;; + cel) + yq -i '.apiVersion = "policies.kyverno.io/v1" | .kind = "GeneratingPolicy" | .spec = {"generation":[{"expression":"generator.Apply(\"v1\", \"configmaps\", \"default\", [])"}]}' "$root/k8s/providers/prod/apps/generate.yaml" + want='unevaluated CEL' + ;; esac printf ' - generate.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" case "$target" in - foreign-group|foreign-version|unrelated-foreach) expect_pass "a bounded $target generator does not match a consumer schema" "$root" '2 consumer(s)' ;; + foreign-group | foreign-version | unrelated-foreach) expect_pass "a bounded $target generator does not match a consumer schema" "$root" '2 consumer(s)' ;; *) expect_refusal "$target generation cannot hide an unseen consumer instance" "$root" "$want" 'UNKNOWN' ;; esac done @@ -1910,7 +1989,7 @@ case "$PARTIAL_RUNTIME:$*" in esac SH chmod +x "$WORK/partial-runtime-$boundary-bin/yq" - case "$boundary" in clone) want='could not bound cloning of Tenant';; templates) want='could not read ResourceSet resourcesTemplate';; esac + case "$boundary" in clone) want='could not bound cloning of Tenant' ;; templates) want='could not read ResourceSet resourcesTemplate' ;; esac REAL_YQ="$(command -v yq)" PARTIAL_RUNTIME="$boundary" PATH="$WORK/partial-runtime-$boundary-bin:$PATH" \ expect_refusal "partial $boundary reader cannot clear a runtime chain" "$root" "$want" 'UNKNOWN' done @@ -2002,10 +2081,15 @@ spec: YAML want='admission mutation' case "$field" in - clone) yq -i 'del(.spec.resources[0].spec.rules[0].mutate) | .spec.resources[0].spec.rules[0].match.any[0].resources.kinds = ["Namespace"] | .spec.resources[0].spec.rules[0].generate.cloneList = {"kinds":["<< inputs.schema >>"],"namespace":"existing"}' "$root/k8s/providers/prod/apps/resource-set.yaml"; want='consumer generation' ;; - native-group|native-resource) yq -i '.spec.resources[0] = {"apiVersion":"admissionregistration.k8s.io/v1","kind":"MutatingWebhookConfiguration","metadata":{"name":"runtime"},"webhooks":[{"name":"runtime.example.test","clientConfig":{"url":"https://webhook.example.test/"},"admissionReviewVersions":["v1"],"sideEffects":"None","rules":[{"apiGroups":["source.toolkit.fluxcd.io"],"apiVersions":["v1"],"operations":["CREATE","UPDATE"],"resources":["ocirepositories"]}]}]}' "$root/k8s/providers/prod/apps/resource-set.yaml" + clone) + yq -i 'del(.spec.resources[0].spec.rules[0].mutate) | .spec.resources[0].spec.rules[0].match.any[0].resources.kinds = ["Namespace"] | .spec.resources[0].spec.rules[0].generate.cloneList = {"kinds":["<< inputs.schema >>"],"namespace":"existing"}' "$root/k8s/providers/prod/apps/resource-set.yaml" + want='consumer generation' + ;; + native-group | native-resource) + yq -i '.spec.resources[0] = {"apiVersion":"admissionregistration.k8s.io/v1","kind":"MutatingWebhookConfiguration","metadata":{"name":"runtime"},"webhooks":[{"name":"runtime.example.test","clientConfig":{"url":"https://webhook.example.test/"},"admissionReviewVersions":["v1"],"sideEffects":"None","rules":[{"apiGroups":["source.toolkit.fluxcd.io"],"apiVersions":["v1"],"operations":["CREATE","UPDATE"],"resources":["ocirepositories"]}]}]}' "$root/k8s/providers/prod/apps/resource-set.yaml" if [ "$field" = native-group ]; then yq -i '.spec.resources[0].webhooks[0].rules[0].apiGroups = ["<< inputs.affectedGroup >>"]' "$root/k8s/providers/prod/apps/resource-set.yaml"; else yq -i '.spec.resources[0].webhooks[0].rules[0].resources = ["<< inputs.affectedResource >>"]' "$root/k8s/providers/prod/apps/resource-set.yaml"; fi - want='native admission mutation' ;; + want='native admission mutation' + ;; esac printf ' - resource-set.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_refusal "a ResourceSet policy $field contract must remain literal" "$root" "$want" 'UNKNOWN' @@ -2041,13 +2125,22 @@ spec: YAML want='nested source reference' case "$field" in - kind) yq -i '.spec.resources[0].apiVersion = "kro.run/v1alpha1" | .spec.resources[0].kind = "<< inputs.resourceKind >>" | .spec.resources[0].spec = {"name":"generated"}' "$root/k8s/providers/prod/apps/resource-set.yaml"; want='ResourceSet object types' ;; - api-version) yq -i '.spec.resources[0].kind = "Tenant" | .spec.resources[0].apiVersion = "<< inputs.group >>/v1alpha1" | .spec.resources[0].spec = {"name":"generated"}' "$root/k8s/providers/prod/apps/resource-set.yaml"; want='ResourceSet object types' ;; + kind) + yq -i '.spec.resources[0].apiVersion = "kro.run/v1alpha1" | .spec.resources[0].kind = "<< inputs.resourceKind >>" | .spec.resources[0].spec = {"name":"generated"}' "$root/k8s/providers/prod/apps/resource-set.yaml" + want='ResourceSet object types' + ;; + api-version) + yq -i '.spec.resources[0].kind = "Tenant" | .spec.resources[0].apiVersion = "<< inputs.group >>/v1alpha1" | .spec.resources[0].spec = {"name":"generated"}' "$root/k8s/providers/prod/apps/resource-set.yaml" + want='ResourceSet object types' + ;; source-kind) yq -i '.spec.resources[0].spec.sourceRef = {"kind":"<< inputs.sourceKind >>","name":"flux-system"}' "$root/k8s/providers/prod/apps/resource-set.yaml" ;; source-name) yq -i '.spec.resources[0].spec.sourceRef.name = "<< inputs.sourceName >>"' "$root/k8s/providers/prod/apps/resource-set.yaml" ;; source-namespace) yq -i '.spec.resources[0].spec.sourceRef.name = "flux-system" | .spec.resources[0].spec.sourceRef.namespace = "<< inputs.namespace >>"' "$root/k8s/providers/prod/apps/resource-set.yaml" ;; inherited-namespace) yq -i '.spec.resources[0].spec.sourceRef.name = "flux-system" | .spec.resources[0].metadata.namespace = "<< inputs.namespace >>"' "$root/k8s/providers/prod/apps/resource-set.yaml" ;; - key) yq -i '.spec.resources[0]["<< inputs.typeField >>"] = .spec.resources[0].kind | del(.spec.resources[0].kind)' "$root/k8s/providers/prod/apps/resource-set.yaml"; want='mapping key' ;; + key) + yq -i '.spec.resources[0]["<< inputs.typeField >>"] = .spec.resources[0].kind | del(.spec.resources[0].kind)' "$root/k8s/providers/prod/apps/resource-set.yaml" + want='mapping key' + ;; esac case "$placement" in steps) yq -i '.spec.steps = [{"name":"sources","resources":.spec.resources}] | del(.spec.resources)' "$root/k8s/providers/prod/apps/resource-set.yaml" ;; @@ -2122,13 +2215,14 @@ regression_platform_source_aliases() { namespace=flux-system url=oci://ghcr.io/devantler-tech/platform/manifests case "$placement" in - cross-root|reverse-root) source_file="$root/k8s/providers/prod/infrastructure/platform-alias.yaml" ;; + cross-root | reverse-root) source_file="$root/k8s/providers/prod/infrastructure/platform-alias.yaml" ;; overlay) source_file="$root/k8s/clusters/prod/platform-alias.yaml" ;; explicit-namespace) namespace=other ;; trailing-slash) url="$url/" ;; unresolved-url) # shellcheck disable=SC2016 # Flux resolves this literal variable, not the shell. - url='oci://ghcr.io/${REPOSITORY}/manifests' ;; + url='oci://ghcr.io/${REPOSITORY}/manifests' + ;; esac cat >"$source_file" <>"$root/k8s/providers/prod/apps/kustomization.yaml" if [ "$placement" = unresolved-url ]; then diff --git a/scripts/tests/test-guard-coroot-baseline-context.sh b/scripts/tests/test-guard-coroot-baseline-context.sh index 3717216f64..a83250f6fc 100755 --- a/scripts/tests/test-guard-coroot-baseline-context.sh +++ b/scripts/tests/test-guard-coroot-baseline-context.sh @@ -83,7 +83,10 @@ check() { fi printf 'ok: %s\n' "${name}" } -use() { rm -f "${scratch}"/input.*.json; cp "${scratch}/$1.json" "${scratch}/input.json"; } +use() { + rm -f "${scratch}"/input.*.json + cp "${scratch}/$1.json" "${scratch}/input.json" +} use unhardened check 'label absent disarms without reading the cluster' before-publish pass api-error disabled @@ -155,23 +158,32 @@ reads="$(cat "${scratch}/count")" jq '.items[0].metadata.resourceVersion = "101"' "${scratch}/hardened.json" >"${scratch}/v101.json" jq '.items[0].metadata.resourceVersion = "102"' "${scratch}/hardened.json" >"${scratch}/v102.json" jq '.items[1].metadata.resourceVersion = "101"' "${scratch}/v101.json" >"${scratch}/two-objects.json" -use hardened; cp "${scratch}/v101.json" "${scratch}/input.2.json"; cp "${scratch}/v101.json" "${scratch}/input.3.json" +use hardened +cp "${scratch}/v101.json" "${scratch}/input.2.json" +cp "${scratch}/v101.json" "${scratch}/input.3.json" cp "${scratch}/v101.json" "${scratch}/input.4.json" check 'one write on one template is tolerated' after-reconcile pass normal enabled -use hardened; cp "${scratch}/v101.json" "${scratch}/input.2.json"; cp "${scratch}/v102.json" "${scratch}/input.3.json" +use hardened +cp "${scratch}/v101.json" "${scratch}/input.2.json" +cp "${scratch}/v102.json" "${scratch}/input.3.json" check 'repeated writes on one template are a loop' after-reconcile fail normal enabled 'repeated owner writes' -use hardened; for n in 2 3 4; do cp "${scratch}/two-objects.json" "${scratch}/input.${n}.json"; done +use hardened +for n in 2 3 4; do cp "${scratch}/two-objects.json" "${scratch}/input.${n}.json"; done check 'one write on each of two templates is not a loop' after-reconcile pass normal enabled jq '.items[3].metadata.generation = 4 | .items[3].status.observedGeneration = 4' \ "${scratch}/hardened.json" >"${scratch}/regen.json" -use hardened; cp "${scratch}/regen.json" "${scratch}/input.3.json" +use hardened +cp "${scratch}/regen.json" "${scratch}/input.3.json" check 'a template rewrite during observation fails' after-reconcile fail normal enabled 'rewrote or replaced' jq '.items[3].metadata.uid = "uid-replacement"' "${scratch}/hardened.json" >"${scratch}/replaced.json" -use hardened; cp "${scratch}/replaced.json" "${scratch}/input.2.json" +use hardened +cp "${scratch}/replaced.json" "${scratch}/input.2.json" check 'a replaced template during observation fails' after-reconcile fail normal enabled 'rewrote or replaced' -use hardened; cp "${scratch}/unhardened.json" "${scratch}/input.2.json" +use hardened +cp "${scratch}/unhardened.json" "${scratch}/input.2.json" check 'an operator stripping the fields fails' after-reconcile fail normal enabled 'removed an admitted field' -use hardened; cp "${scratch}/unready.json" "${scratch}/input.3.json" +use hardened +cp "${scratch}/unready.json" "${scratch}/input.3.json" jq '.items |= map(.spec.template.spec.securityContext.fsGroupChangePolicy = "OnRootMismatch" | .spec.template.spec.containers |= map(.securityContext.seLinuxOptions = {}))' \ "${scratch}/input.3.json" >"${scratch}/tmp.json" && mv "${scratch}/tmp.json" "${scratch}/input.3.json" @@ -191,7 +203,8 @@ grep -qxF "preexisting_unready=${record}" "${scratch}/outputs" printf 'FAIL: a pre-existing unready template must receive three stability samples\n' >&2 exit 1 } -use degraded; cp "${scratch}/hardened.json" "${scratch}/input.4.json" +use degraded +cp "${scratch}/hardened.json" "${scratch}/input.4.json" check 'a template that recovers during observation leaves no tolerated record' before-publish pass normal enabled grep -qx 'rollout_required=true' "${scratch}/outputs" if grep -q '^preexisting_unready=' "${scratch}/outputs"; then @@ -200,26 +213,33 @@ if grep -q '^preexisting_unready=' "${scratch}/outputs"; then fi jq '.items[4].metadata.generation = 4 | .items[4].status.observedGeneration = 4' \ "${scratch}/degraded.json" >"${scratch}/degraded-regen.json" -use degraded; cp "${scratch}/degraded-regen.json" "${scratch}/input.3.json" +use degraded +cp "${scratch}/degraded-regen.json" "${scratch}/input.3.json" check 'an unready template rewritten during observation is refused' before-publish fail normal enabled 'rewrote or replaced' jq '.items[4].metadata.resourceVersion = "101"' "${scratch}/degraded.json" >"${scratch}/degraded-v101.json" jq '.items[4].metadata.resourceVersion = "102"' "${scratch}/degraded.json" >"${scratch}/degraded-v102.json" -use degraded; cp "${scratch}/degraded-v101.json" "${scratch}/input.2.json"; cp "${scratch}/degraded-v102.json" "${scratch}/input.3.json" +use degraded +cp "${scratch}/degraded-v101.json" "${scratch}/input.2.json" +cp "${scratch}/degraded-v102.json" "${scratch}/input.3.json" check 'an unready template written repeatedly is refused' before-publish fail normal enabled 'repeated owner writes' jq '.items[4].metadata.generation = 4' "${scratch}/degraded.json" >"${scratch}/degraded-unobserved.json" use degraded-unobserved check 'an unready template with an unobserved generation is refused' before-publish fail normal enabled 'has not observed the latest generation' -use degraded; cp "${scratch}/unhardened.json" "${scratch}/input.3.json" +use degraded +cp "${scratch}/unhardened.json" "${scratch}/input.3.json" check 'an unready template losing its fields during observation is refused' before-publish fail normal enabled 'removed an admitted field' jq '.items[4].metadata.ownerReferences = []' "${scratch}/degraded.json" >"${scratch}/degraded-orphan.json" -use degraded; cp "${scratch}/degraded-orphan.json" "${scratch}/input.2.json" +use degraded +cp "${scratch}/degraded-orphan.json" "${scratch}/input.2.json" check 'an unready template losing its owner during observation is refused' before-publish fail normal enabled 'lost its operator owner' jq '.items[1].status.readyReplicas = 0' "${scratch}/degraded.json" >"${scratch}/degraded-second.json" jq '.items[4].status.readyReplicas = 0 | .items[4].status.availableReplicas = 0' "${scratch}/degraded.json" >"${scratch}/degraded-worse.json" -use degraded; cp "${scratch}/degraded-second.json" "${scratch}/input.3.json" +use degraded +cp "${scratch}/degraded-second.json" "${scratch}/input.3.json" check 'a second template losing readiness during observation is refused' before-publish fail normal enabled 'lost readiness' -use degraded; cp "${scratch}/degraded-worse.json" "${scratch}/input.2.json" +use degraded +cp "${scratch}/degraded-worse.json" "${scratch}/input.2.json" check 'an unready template getting less ready during observation is refused' before-publish fail normal enabled 'lost readiness' # After reconcile the tolerated record admits only that same object, no worse. @@ -246,9 +266,12 @@ use degraded-replaced check 'the tolerated record lapses when the template is replaced' after-reconcile fail normal enabled 'did not all reach both fields' use degraded-worse check 'a tolerated template that got less ready fails' after-reconcile fail normal enabled 'did not all reach both fields' -use degraded; cp "${scratch}/degraded-worse.json" "${scratch}/input.3.json" +use degraded +cp "${scratch}/degraded-worse.json" "${scratch}/input.3.json" check 'a tolerated template losing readiness during observation fails' after-reconcile fail normal enabled 'lost readiness' -use degraded; cp "${scratch}/degraded-v101.json" "${scratch}/input.2.json"; cp "${scratch}/degraded-v102.json" "${scratch}/input.3.json" +use degraded +cp "${scratch}/degraded-v101.json" "${scratch}/input.2.json" +cp "${scratch}/degraded-v102.json" "${scratch}/input.3.json" check 'a tolerated template is still held to the rewrite loop check' after-reconcile fail normal enabled 'repeated owner writes' jq '.items |= map(.spec.template.spec.securityContext |= del(.fsGroupChangePolicy))' "${scratch}/degraded.json" >"${scratch}/degraded-stripped.json" use degraded-stripped diff --git a/scripts/tests/test-guard-crossplane-provider-name-parity.sh b/scripts/tests/test-guard-crossplane-provider-name-parity.sh index 2e595c9f09..7e599e450f 100755 --- a/scripts/tests/test-guard-crossplane-provider-name-parity.sh +++ b/scripts/tests/test-guard-crossplane-provider-name-parity.sh @@ -235,21 +235,25 @@ for shape in flow whole-flow alias merge; do printf '%s\n' 'apiVersion: pkg.crossplane.io/v1' 'kind: Provider' \ 'metadata: { name: upbound-provider-family-aws }' \ 'spec: { package: xpkg.upbound.io/upbound/provider-family-aws:v2.6.1 }' \ - >"$t/providers/canonical.yaml" ;; + >"$t/providers/canonical.yaml" + ;; whole-flow) printf '%s\n' '{apiVersion: pkg.crossplane.io/v1, kind: Provider, metadata: {name: upbound-provider-family-aws}, spec: {package: "xpkg.upbound.io/upbound/provider-family-aws:v2.6.1"}}' \ - >"$t/providers/canonical.yml" ;; + >"$t/providers/canonical.yml" + ;; alias) printf '%s\n' 'apiVersion: pkg.crossplane.io/v1' 'kind: Provider' \ 'identity: &metadata {name: upbound-provider-family-aws}' \ 'packageRef: &package {package: "xpkg.upbound.io/upbound/provider-family-aws:v2.6.1"}' \ - 'metadata: *metadata' 'spec: *package' >"$t/providers/canonical.yaml" ;; + 'metadata: *metadata' 'spec: *package' >"$t/providers/canonical.yaml" + ;; merge) printf '%s\n' 'apiVersion: pkg.crossplane.io/v1' 'kind: Provider' \ 'defaults: &defaults {name: shortened}' \ 'metadata: {name: upbound-provider-family-aws, <<: *defaults}' \ 'spec: {package: "xpkg.upbound.io/upbound/provider-family-aws:v2.6.1"}' \ - >"$t/providers/canonical.yaml" ;; + >"$t/providers/canonical.yaml" + ;; esac run_guard "$t" assert_rc "$shape canonical Provider is checked" 0 "$GUARD_RC" diff --git a/scripts/tests/test-guard-ghcr-fanout-component-gate.sh b/scripts/tests/test-guard-ghcr-fanout-component-gate.sh index 3155839dfe..f61970fcfc 100755 --- a/scripts/tests/test-guard-ghcr-fanout-component-gate.sh +++ b/scripts/tests/test-guard-ghcr-fanout-component-gate.sh @@ -44,7 +44,10 @@ trap 'rm -rf "$scratch"' EXIT failures=0 assertions=0 -[ -x "$guard" ] || { printf 'FAIL: %s is not executable\n' "$guard"; exit 1; } +[ -x "$guard" ] || { + printf 'FAIL: %s is not executable\n' "$guard" + exit 1 +} run_guard() { # # Capture the status with `if`: this file runs under `set -uo pipefail` with @@ -139,7 +142,8 @@ write_app_kustomization() { # write_kustomization ... (apps not listed are staged off) write_kustomization() { - local root="$1"; shift + local root="$1" + shift { printf -- '---\napiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n' local a @@ -155,7 +159,8 @@ write_kustomization() { # write_fanout ... write_fanout() { - local root="$1"; shift + local root="$1" + shift { printf '#!/usr/bin/env bash\nreadonly -a FANOUT_NAMESPACES=(\n' local n diff --git a/scripts/tests/test-guard-helm-post-renderers.sh b/scripts/tests/test-guard-helm-post-renderers.sh index ebad2c1a35..275991a0ff 100755 --- a/scripts/tests/test-guard-helm-post-renderers.sh +++ b/scripts/tests/test-guard-helm-post-renderers.sh @@ -964,9 +964,9 @@ yq -i '.spec.chart.spec.chart = "sdk-sensitive"' "$TREE/k8s/controllers/test/hel # Demonstrate the divergence with both real rendering commands and the actual # patch, independently of the guard's refusal of a mismatched renderer binary. mkdir -p "$scratch/cli-divergence" -yq -o=json '.spec.postRenderers[0].kustomize' "$TREE/k8s/controllers/test/helm-release.yaml" | \ +yq -o=json '.spec.postRenderers[0].kustomize' "$TREE/k8s/controllers/test/helm-release.yaml" | jq '. + {apiVersion:"kustomize.config.k8s.io/v1beta1",kind:"Kustomization",resources:["rendered.yaml"]}' \ - >"$scratch/cli-divergence/kustomization.yaml" + >"$scratch/cli-divergence/kustomization.yaml" for cli_mode in install upgrade; do cli_args=(template flux-operator "$scratch/charts/sdk-sensitive" --namespace flux-system) [ "$cli_mode" != upgrade ] || cli_args+=(--is-upgrade) diff --git a/scripts/tests/test-guard-kubescape-scan-scope.sh b/scripts/tests/test-guard-kubescape-scan-scope.sh index cbf9a3359a..87e965437c 100755 --- a/scripts/tests/test-guard-kubescape-scan-scope.sh +++ b/scripts/tests/test-guard-kubescape-scan-scope.sh @@ -93,7 +93,10 @@ expect "row without a reason fails" 1 "kube-public is listed without a reason" \ "${REAL_HR}" "${tmp}/no-reason.tsv" # A duplicated row. -{ cat "${REAL_TSV}"; grep '^kubeconfig ' "${REAL_TSV}"; } >"${tmp}/dup.tsv" +{ + cat "${REAL_TSV}" + grep '^kubeconfig ' "${REAL_TSV}" +} >"${tmp}/dup.tsv" [ "$(grep -c '^kubeconfig ' "${tmp}/dup.tsv")" -eq 2 ] || { printf 'FAIL: duplicate fixture did not land\n' >&2 exit 1 diff --git a/scripts/tests/test-guard-limitrange-premise.sh b/scripts/tests/test-guard-limitrange-premise.sh index 58c30e3def..46692c0bd6 100755 --- a/scripts/tests/test-guard-limitrange-premise.sh +++ b/scripts/tests/test-guard-limitrange-premise.sh @@ -418,7 +418,6 @@ make_provider "$nolayer" nolayerlike blind-dns kube-system \ rm -rf "$nolayer/clusters" expect 'missing-layer-definitions-is-exit-2' 2 "$nolayer" 'flux-kustomization' - # --- 19. A CLUSTER-SCOPED layer (bootstrap) also shields --------------------- # The bootstrap layer root is `clusters/__CLUSTER__/bootstrap`, not # `providers/__PROVIDER__/...`, yet it reaches a provider overlay one hop in. The diff --git a/scripts/tests/test-guard-longhorn-ui-baseline-context.sh b/scripts/tests/test-guard-longhorn-ui-baseline-context.sh index c1ee2930c0..2fb7998c99 100755 --- a/scripts/tests/test-guard-longhorn-ui-baseline-context.sh +++ b/scripts/tests/test-guard-longhorn-ui-baseline-context.sh @@ -153,7 +153,7 @@ grep -qx 'rollout_required=false' "${scratch}/outputs" # artifact boundary. A standalone passing checker is not an enforced rollout. for workflow in .github/actions/deploy-prod/action.yml .github/workflows/dr-rebuild.yaml; do yq -o=json '.runs.steps // .jobs.rebuild.steps' "${root_dir}/${workflow}" | - jq -e ' + jq -e ' map(.id // "") as $ids | ($ids | index("longhorn_ui_baseline")) as $before | ($ids | index("publish_platform_manifest")) as $publish | diff --git a/scripts/tests/test-guard-oci-repository-verify.sh b/scripts/tests/test-guard-oci-repository-verify.sh index 21b613c262..4e5165f178 100755 --- a/scripts/tests/test-guard-oci-repository-verify.sh +++ b/scripts/tests/test-guard-oci-repository-verify.sh @@ -30,7 +30,10 @@ fail() { } pass() { printf 'ok: %s\n' "$*"; } -[ -x "$GUARD" ] || { fail "guard not executable at $GUARD"; exit 1; } +[ -x "$GUARD" ] || { + fail "guard not executable at $GUARD" + exit 1 +} WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT @@ -121,55 +124,68 @@ expect_accepted 'a verified in-scope OCIRepository with one identity is accepted # --- GREEN: trusted tenants may follow the platform-owned release boundary --- root="$(fresh_root trusted-stream)" -repo_doc wedding-app 'oci://ghcr.io/devantler-tech/wedding-app/manifests' "$(good_verify)" >"$root/wedding.yaml"; add_resource "$root" wedding.yaml +repo_doc wedding-app 'oci://ghcr.io/devantler-tech/wedding-app/manifests' "$(good_verify)" >"$root/wedding.yaml" +add_resource "$root" wedding.yaml expect_accepted 'a trusted tenant using the governed semver stream is accepted' "$root" # --- RED: trusted tenant mobility must not silently regress to platform pins --- root="$(fresh_root trusted-stream-tag)" -repo_doc wedding-app 'oci://ghcr.io/devantler-tech/wedding-app/manifests' "$(good_verify)" >"$root/wedding.yaml"; add_resource "$root" wedding.yaml +repo_doc wedding-app 'oci://ghcr.io/devantler-tech/wedding-app/manifests' "$(good_verify)" >"$root/wedding.yaml" +add_resource "$root" wedding.yaml yq -i '.spec.ref = {"tag": "v1.15.11"}' "$root/wedding.yaml" expect_refused 'a trusted tenant fixed tag is refused by name' "$root" 'OCIRepository wedding-app (oci://ghcr.io/devantler-tech/wedding-app/manifests) must follow the governed release stream with exactly spec.ref.semver: >=1.0.0' root="$(fresh_root trusted-stream-digest)" -repo_doc ascoachingogvaner 'oci://ghcr.io/devantler-tech/ascoachingogvaner/manifests' "$(good_verify)" >"$root/ascoachingogvaner.yaml"; add_resource "$root" ascoachingogvaner.yaml +repo_doc ascoachingogvaner 'oci://ghcr.io/devantler-tech/ascoachingogvaner/manifests' "$(good_verify)" >"$root/ascoachingogvaner.yaml" +add_resource "$root" ascoachingogvaner.yaml yq -i '.spec.ref = {"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}' "$root/ascoachingogvaner.yaml" expect_refused 'a trusted tenant digest pin is refused by name' "$root" 'OCIRepository ascoachingogvaner (oci://ghcr.io/devantler-tech/ascoachingogvaner/manifests) must follow the governed release stream with exactly spec.ref.semver: >=1.0.0' root="$(fresh_root templated-trusted-stream-tag)" # shellcheck disable=SC2016 # KRO placeholder is intentionally literal. -repo_doc '${schema.spec.name}' 'oci://ghcr.io/devantler-tech/${schema.spec.name}/manifests' "$(good_verify)" >"$root/tenant-template.yaml"; add_resource "$root" tenant-template.yaml +repo_doc '${schema.spec.name}' 'oci://ghcr.io/devantler-tech/${schema.spec.name}/manifests' "$(good_verify)" >"$root/tenant-template.yaml" +add_resource "$root" tenant-template.yaml yq -i '.spec.ref = {"tag": "v1.0.0"}' "$root/tenant-template.yaml" # shellcheck disable=SC2016 # Expected diagnostic preserves the KRO placeholder. expect_refused 'a templated trusted tenant fixed tag is refused by name' "$root" 'OCIRepository ${schema.spec.name} (oci://ghcr.io/devantler-tech/${schema.spec.name}/manifests) must follow the governed release stream with exactly spec.ref.semver: >=1.0.0' # --- RED: no spec.verify at all (AC1) --- root="$(fresh_root noverify)" -repo_doc bare 'oci://ghcr.io/devantler-tech/bare/manifests' '' >"$root/bare.yaml"; add_resource "$root" bare.yaml +repo_doc bare 'oci://ghcr.io/devantler-tech/bare/manifests' '' >"$root/bare.yaml" +add_resource "$root" bare.yaml expect_refused 'no spec.verify is refused by name' "$root" 'OCIRepository bare (oci://ghcr.io/devantler-tech/bare/manifests) has NO spec.verify' # --- RED: verify with zero identity entries (AC2) --- root="$(fresh_root zero)" -repo_doc zero 'oci://ghcr.io/devantler-tech/zero/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity: []')" >"$root/zero.yaml"; add_resource "$root" zero.yaml +repo_doc zero 'oci://ghcr.io/devantler-tech/zero/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity: []')" >"$root/zero.yaml" +add_resource "$root" zero.yaml expect_refused 'zero matchOIDCIdentity entries is refused' "$root" 'OCIRepository zero (oci://ghcr.io/devantler-tech/zero/manifests) has ZERO matchOIDCIdentity entries' # --- RED: verify with no matchOIDCIdentity key at all --- root="$(fresh_root nokey)" -repo_doc nokey 'oci://ghcr.io/devantler-tech/nokey/manifests' "$(printf ' verify:\n provider: cosign')" >"$root/nokey.yaml"; add_resource "$root" nokey.yaml +repo_doc nokey 'oci://ghcr.io/devantler-tech/nokey/manifests' "$(printf ' verify:\n provider: cosign')" >"$root/nokey.yaml" +add_resource "$root" nokey.yaml expect_refused 'a verify block without matchOIDCIdentity is refused' "$root" 'OCIRepository nokey (oci://ghcr.io/devantler-tech/nokey/manifests) has no matchOIDCIdentity sequence' # --- RED: two identity entries (Flux ORs them) --- root="$(fresh_root two)" -repo_doc two 'oci://ghcr.io/devantler-tech/two/manifests' "$(good_verify; printf '\n - issuer: %s\n subject: %s' "'.*'" "'.*'")" >"$root/two.yaml"; add_resource "$root" two.yaml +repo_doc two 'oci://ghcr.io/devantler-tech/two/manifests' "$( + good_verify + printf '\n - issuer: %s\n subject: %s' "'.*'" "'.*'" +)" >"$root/two.yaml" +add_resource "$root" two.yaml expect_refused 'a second identity entry is refused as a widening' "$root" 'OCIRepository two (oci://ghcr.io/devantler-tech/two/manifests) has 2 matchOIDCIdentity entries' # --- RED: a provider other than cosign --- root="$(fresh_root notation)" -repo_doc notation 'oci://ghcr.io/devantler-tech/notation/manifests' "$(printf ' verify:\n provider: notation\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "$ISSUER" "$SUBJECT")" >"$root/notation.yaml"; add_resource "$root" notation.yaml +repo_doc notation 'oci://ghcr.io/devantler-tech/notation/manifests' "$(printf ' verify:\n provider: notation\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "$ISSUER" "$SUBJECT")" >"$root/notation.yaml" +add_resource "$root" notation.yaml expect_refused 'a non-cosign provider is refused' "$root" "OCIRepository notation (oci://ghcr.io/devantler-tech/notation/manifests) verifies with provider 'notation'" # --- RED: an entry with an empty subject --- root="$(fresh_root nosubject)" -repo_doc nosubject 'oci://ghcr.io/devantler-tech/nosubject/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s' "$ISSUER")" >"$root/nosubject.yaml"; add_resource "$root" nosubject.yaml +repo_doc nosubject 'oci://ghcr.io/devantler-tech/nosubject/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s' "$ISSUER")" >"$root/nosubject.yaml" +add_resource "$root" nosubject.yaml expect_refused 'an identity entry without a subject is refused' "$root" 'OCIRepository nosubject (oci://ghcr.io/devantler-tech/nosubject/manifests) has a matchOIDCIdentity entry missing a non-empty issuer or subject' # --- RED: discovery at depth — an unverified OCIRepository nested as a template --- @@ -199,88 +215,108 @@ expect_refused 'an unverified OCIRepository nested inside a template is discover # --- RED: discovery across documents — the second document of a multi-doc file --- root="$(fresh_root multidoc)" -{ printf -- 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: second\n---\n'; repo_doc second 'oci://ghcr.io/devantler-tech/second/manifests' ''; } >"$root/second.yaml"; add_resource "$root" second.yaml +{ + printf -- 'apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: second\n---\n' + repo_doc second 'oci://ghcr.io/devantler-tech/second/manifests' '' +} >"$root/second.yaml" +add_resource "$root" second.yaml expect_refused 'an unverified OCIRepository in a later YAML document is discovered' "$root" 'OCIRepository second (oci://ghcr.io/devantler-tech/second/manifests) has NO spec.verify' # --- RED: the render is judged, not the source — bypasses a source scan had --- root="$(fresh_root binarytag)" -repo_doc bin 'oci://ghcr.io/devantler-tech/bin/manifests' '' | sed 's/^kind: OCIRepository$/kind: !!binary T0NJUmVwb3NpdG9yeQ==/' >"$root/bin.yaml"; add_resource "$root" bin.yaml +repo_doc bin 'oci://ghcr.io/devantler-tech/bin/manifests' '' | sed 's/^kind: OCIRepository$/kind: !!binary T0NJUmVwb3NpdG9yeQ==/' >"$root/bin.yaml" +add_resource "$root" bin.yaml assert_renders "$root" 'oci://ghcr.io/devantler-tech/bin/manifests' && expect_refused 'a !!binary-tagged kind is resolved by the render and refused' "$root" 'OCIRepository bin (oci://ghcr.io/devantler-tech/bin/manifests) has NO spec.verify' root="$(fresh_root alias)" -{ printf 'apiVersion: source.toolkit.fluxcd.io/v1\nmetadata:\n name: al\n namespace: al\n annotations:\n note: &k OCIRepository\nkind: *k\nspec:\n interval: 10m\n url: oci://ghcr.io/devantler-tech/al/manifests\n ref:\n semver: ">=1.0.0"\n'; } >"$root/al.yaml"; add_resource "$root" al.yaml +{ printf 'apiVersion: source.toolkit.fluxcd.io/v1\nmetadata:\n name: al\n namespace: al\n annotations:\n note: &k OCIRepository\nkind: *k\nspec:\n interval: 10m\n url: oci://ghcr.io/devantler-tech/al/manifests\n ref:\n semver: ">=1.0.0"\n'; } >"$root/al.yaml" +add_resource "$root" al.yaml assert_renders "$root" 'oci://ghcr.io/devantler-tech/al/manifests' && expect_refused 'an aliased kind is resolved by the render and refused' "$root" 'OCIRepository al (oci://ghcr.io/devantler-tech/al/manifests) has NO spec.verify' root="$(fresh_root json)" -printf '{"apiVersion":"source.toolkit.fluxcd.io/v1","kind":"OCIRepository","metadata":{"name":"js","namespace":"js"},"spec":{"interval":"10m","url":"oci://ghcr.io/devantler-tech/js/manifests","ref":{"semver":">=1.0.0"}}}\n' >"$root/js.json"; add_resource "$root" js.json +printf '{"apiVersion":"source.toolkit.fluxcd.io/v1","kind":"OCIRepository","metadata":{"name":"js","namespace":"js"},"spec":{"interval":"10m","url":"oci://ghcr.io/devantler-tech/js/manifests","ref":{"semver":">=1.0.0"}}}\n' >"$root/js.json" +add_resource "$root" js.json assert_renders "$root" 'oci://ghcr.io/devantler-tech/js/manifests' && expect_refused 'a .json resource is rendered and refused' "$root" 'OCIRepository js (oci://ghcr.io/devantler-tech/js/manifests) has NO spec.verify' root="$(fresh_root noext)" -repo_doc noext 'oci://ghcr.io/devantler-tech/noext/manifests' '' >"$root/ocirepo"; add_resource "$root" ocirepo +repo_doc noext 'oci://ghcr.io/devantler-tech/noext/manifests' '' >"$root/ocirepo" +add_resource "$root" ocirepo assert_renders "$root" 'oci://ghcr.io/devantler-tech/noext/manifests' && expect_refused 'an extension-less resource is rendered and refused' "$root" 'OCIRepository noext (oci://ghcr.io/devantler-tech/noext/manifests) has NO spec.verify' root="$(fresh_root symlink)" -repo_doc sym 'oci://ghcr.io/devantler-tech/sym/manifests' '' >"$root/real.txt"; ln -s real.txt "$root/sym.yaml"; add_resource "$root" sym.yaml +repo_doc sym 'oci://ghcr.io/devantler-tech/sym/manifests' '' >"$root/real.txt" +ln -s real.txt "$root/sym.yaml" +add_resource "$root" sym.yaml assert_renders "$root" 'oci://ghcr.io/devantler-tech/sym/manifests' && expect_refused 'a symlinked resource is rendered and refused' "$root" 'OCIRepository sym (oci://ghcr.io/devantler-tech/sym/manifests) has NO spec.verify' root="$(fresh_root patchremove)" -repo_doc pr 'oci://ghcr.io/devantler-tech/pr/manifests' "$(good_verify)" >"$root/pr.yaml"; add_resource "$root" pr.yaml +repo_doc pr 'oci://ghcr.io/devantler-tech/pr/manifests' "$(good_verify)" >"$root/pr.yaml" +add_resource "$root" pr.yaml printf 'patches:\n - target:\n kind: OCIRepository\n name: pr\n patch: |\n - op: remove\n path: /spec/verify\n' >>"$root/kustomization.yaml" expect_refused 'a patch that removes spec.verify is judged on the render' "$root" 'OCIRepository pr (oci://ghcr.io/devantler-tech/pr/manifests) has NO spec.verify' root="$(fresh_root patchappend)" -repo_doc pa 'oci://ghcr.io/devantler-tech/pa/manifests' "$(good_verify)" >"$root/pa.yaml"; add_resource "$root" pa.yaml +repo_doc pa 'oci://ghcr.io/devantler-tech/pa/manifests' "$(good_verify)" >"$root/pa.yaml" +add_resource "$root" pa.yaml printf 'patches:\n - target:\n kind: OCIRepository\n name: pa\n patch: |\n - op: add\n path: /spec/verify/matchOIDCIdentity/-\n value:\n issuer: ".*"\n subject: ".*"\n' >>"$root/kustomization.yaml" expect_refused 'a patch that appends an identity is judged on the render' "$root" 'OCIRepository pa (oci://ghcr.io/devantler-tech/pa/manifests) has 2 matchOIDCIdentity entries' # --- RED: the one identity must be OURS — a foreign issuer or subject passes every # shape check and is invisible to both subject guards --- root="$(fresh_root foreignissuer)" -repo_doc fiss 'oci://ghcr.io/devantler-tech/fiss/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "'^https://accounts\\.google\\.com\$'" "$SUBJECT")" >"$root/fiss.yaml"; add_resource "$root" fiss.yaml +repo_doc fiss 'oci://ghcr.io/devantler-tech/fiss/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "'^https://accounts\\.google\\.com\$'" "$SUBJECT")" >"$root/fiss.yaml" +add_resource "$root" fiss.yaml expect_refused 'a foreign issuer is refused' "$root" "OCIRepository fiss (oci://ghcr.io/devantler-tech/fiss/manifests) trusts issuer '^https://accounts\\.google\\.com\$'" root="$(fresh_root wildcard)" -repo_doc wc 'oci://ghcr.io/devantler-tech/wc/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "$ISSUER" "'.*'")" >"$root/wc.yaml"; add_resource "$root" wc.yaml +repo_doc wc 'oci://ghcr.io/devantler-tech/wc/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "$ISSUER" "'.*'")" >"$root/wc.yaml" +add_resource "$root" wc.yaml expect_refused 'a wildcard subject is refused' "$root" "OCIRepository wc (oci://ghcr.io/devantler-tech/wc/manifests) trusts subject '.*'" root="$(fresh_root foreignorg)" -repo_doc fo 'oci://ghcr.io/devantler-tech/fo/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "$ISSUER" "'^https://github\\.com/evil-org/actions/\\.github/workflows/publish-app\\.yaml@[0-9a-f]{40}\$'")" >"$root/fo.yaml"; add_resource "$root" fo.yaml +repo_doc fo 'oci://ghcr.io/devantler-tech/fo/manifests' "$(printf ' verify:\n provider: cosign\n matchOIDCIdentity:\n - issuer: %s\n subject: %s' "$ISSUER" "'^https://github\\.com/evil-org/actions/\\.github/workflows/publish-app\\.yaml@[0-9a-f]{40}\$'")" >"$root/fo.yaml" +add_resource "$root" fo.yaml expect_refused 'a subject anchored on another organisation is refused' "$root" 'does not anchor on' # --- RED: host case and substituted hosts --- root="$(fresh_root upper)" -repo_doc up 'oci://GHCR.IO/devantler-tech/up/manifests' '' >"$root/up.yaml"; add_resource "$root" up.yaml +repo_doc up 'oci://GHCR.IO/devantler-tech/up/manifests' '' >"$root/up.yaml" +add_resource "$root" up.yaml expect_refused 'an upper-cased registry host is still in scope' "$root" 'OCIRepository up (oci://ghcr.io/devantler-tech/up/manifests) has NO spec.verify' root="$(fresh_root subst)" # The `${REGISTRY}` below is a Flux substitution placeholder, quoted verbatim. # shellcheck disable=SC2016 -repo_doc sub 'oci://${REGISTRY}/devantler-tech/sub/manifests' '' >"$root/sub.yaml"; add_resource "$root" sub.yaml +repo_doc sub 'oci://${REGISTRY}/devantler-tech/sub/manifests' '' >"$root/sub.yaml" +add_resource "$root" sub.yaml expect_refused 'a host decided by a substitution variable is refused as undecidable' "$root" 'decides its registry or organisation through a substitution variable' # --- CONTROLS --- root="$(fresh_root thirdparty)" -repo_doc flux 'oci://ghcr.io/fluxcd/flux-manifests' '' >"$root/flux.yaml"; add_resource "$root" flux.yaml +repo_doc flux 'oci://ghcr.io/fluxcd/flux-manifests' '' >"$root/flux.yaml" +add_resource "$root" flux.yaml expect_accepted 'a third-party OCIRepository without verify is out of scope' "$root" root="$(fresh_root outside)" -mkdir -p "$WORK/elsewhere"; repo_doc outside 'oci://ghcr.io/devantler-tech/outside/manifests' '' >"$WORK/elsewhere/outside.yaml" +mkdir -p "$WORK/elsewhere" +repo_doc outside 'oci://ghcr.io/devantler-tech/outside/manifests' '' >"$WORK/elsewhere/outside.yaml" expect_accepted 'a defect in a root that is not listed is not attributed to the tree' "$root" # --- EXEMPTIONS --- root="$(fresh_root exempt)" -repo_doc unsigned 'oci://ghcr.io/devantler-tech/charts/unsigned' '' >"$root/unsigned.yaml"; add_resource "$root" unsigned.yaml +repo_doc unsigned 'oci://ghcr.io/devantler-tech/charts/unsigned' '' >"$root/unsigned.yaml" +add_resource "$root" unsigned.yaml printf 'oci://ghcr.io/devantler-tech/charts/unsigned\tpublished unsigned; tracked by example#1\n' >"$WORK/exempt.tsv" expect_accepted 'an exempt unverified OCIRepository is admitted' "$root" "$WORK/exempt.tsv" expect_refused 'the same root with no exemption row is refused' "$root" 'OCIRepository unsigned (oci://ghcr.io/devantler-tech/charts/unsigned) has NO spec.verify' printf 'oci://ghcr.io/devantler-tech/charts/unsigned\n' >"$WORK/exempt-noreason.tsv" expect_refused 'an exemption row without a reason is refused, once' "$root" 'carries no reason' "$WORK/exempt-noreason.tsv" -case "$out" in *"has NO spec.verify"*|*"STALE"*) fail 'a reason-less row produced cascading wrong-reason messages';; esac +case "$out" in *"has NO spec.verify"* | *"STALE"*) fail 'a reason-less row produced cascading wrong-reason messages' ;; esac printf 'oci://ghcr.io/devantler-tech/charts/unsigned\t \n' >"$WORK/exempt-emptyreason.tsv" expect_refused 'an exemption row with an empty reason is refused' "$root" 'carries an empty reason' "$WORK/exempt-emptyreason.tsv" @@ -292,18 +328,23 @@ printf 'oci://ghcr.io/devantler-tech/charts/gone\tretired long ago\n' >"$WORK/st expect_refused 'an exemption naming no rendered OCIRepository is STALE' "$root" 'exemption for oci://ghcr.io/devantler-tech/charts/gone is STALE' "$WORK/stale.tsv" root="$(fresh_root stalesuffix)" -repo_doc unsigned 'oci://ghcr.io/devantler-tech/charts/unsigned' '' >"$root/unsigned.yaml"; add_resource "$root" unsigned.yaml +repo_doc unsigned 'oci://ghcr.io/devantler-tech/charts/unsigned' '' >"$root/unsigned.yaml" +add_resource "$root" unsigned.yaml printf 'oci://ghcr.io/devantler-tech/charts/unsigned\treal\ntech/charts/unsigned\tdead\n' >"$WORK/stale-suffix.tsv" expect_refused 'a row that is only a suffix of a rendered URL is STALE (exact match)' "$root" 'exemption for tech/charts/unsigned is STALE' "$WORK/stale-suffix.tsv" root="$(fresh_root gained)" -repo_doc unsigned 'oci://ghcr.io/devantler-tech/charts/unsigned' "$(good_verify)" >"$root/unsigned.yaml"; add_resource "$root" unsigned.yaml +repo_doc unsigned 'oci://ghcr.io/devantler-tech/charts/unsigned' "$(good_verify)" >"$root/unsigned.yaml" +add_resource "$root" unsigned.yaml expect_refused 'an exempt OCIRepository that now verifies reports its exemption as stale' "$root" 'is exempt from verification but carries spec.verify' "$WORK/exempt.tsv" # --- RED: the floor — an empty in-scope result is a claim about the discovery --- -root="$WORK/root-floor"; rm -rf "$root"; mkdir -p "$root" +root="$WORK/root-floor" +rm -rf "$root" +mkdir -p "$root" printf 'apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nresources:\n' >"$root/kustomization.yaml" -repo_doc flux 'oci://ghcr.io/fluxcd/flux-manifests' "$(good_verify)" >"$root/flux.yaml"; add_resource "$root" flux.yaml +repo_doc flux 'oci://ghcr.io/fluxcd/flux-manifests' "$(good_verify)" >"$root/flux.yaml" +add_resource "$root" flux.yaml expect_refused 'zero in-scope OCIRepositories fails the floor' "$root" 'found 0 in-scope OCIRepositor(y/ies)' # --- RED: a root that does not render is UNKNOWN, never clean --- @@ -343,7 +384,8 @@ expect_descriptor_discovered Kustomization # Keep one valid overlay in the fixture: if the guard regresses to `continue` for the # descriptor-less one, the good root satisfies the floor and the guard would pass. -k8s="$WORK/k8s-missing-descriptor"; rm -rf "$k8s" +k8s="$WORK/k8s-missing-descriptor" +rm -rf "$k8s" write_discovery_overlay "$k8s" alpha kustomization.yml good "$(good_verify)" mkdir -p "$k8s/clusters/bravo" if out="$(OCI_VERIFY_K8S_DIR="$k8s" OCI_VERIFY_EXEMPTIONS_FILE="$WORK/no-exemptions.tsv" OCI_VERIFY_EXPECTED_MIN=1 "$GUARD" 2>&1)"; then diff --git a/scripts/tests/test-guard-test-script-exec-bit.sh b/scripts/tests/test-guard-test-script-exec-bit.sh index aac1922166..1d2fa448ef 100755 --- a/scripts/tests/test-guard-test-script-exec-bit.sh +++ b/scripts/tests/test-guard-test-script-exec-bit.sh @@ -6,39 +6,42 @@ work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT git -C "$work" init -q mkdir -p "$work/scripts/tests/fixtures" "$work/scripts" -printf '#!/usr/bin/env bash\nexit 0\n' > "$work/scripts/tests/test-entry.sh" -printf 'library_function() { :; }\n' > "$work/scripts/library.sh" -printf '#!/usr/bin/env bash\nexit 0\n' > "$work/scripts/tests/fixtures/fake.sh" +printf '#!/usr/bin/env bash\nexit 0\n' >"$work/scripts/tests/test-entry.sh" +printf 'library_function() { :; }\n' >"$work/scripts/library.sh" +printf '#!/usr/bin/env bash\nexit 0\n' >"$work/scripts/tests/fixtures/fake.sh" cp "$root/.mega-linter.yml" "$work/.mega-linter.yml" for library in cosign-failure-lib.sh ghcr-auth-lib.sh publish-workflow-approved-revisions.lib.sh refresh-flux-ghcr-auth-safety.sh; do - printf 'library_function() { :; }\n' > "$work/scripts/$library" + printf 'library_function() { :; }\n' >"$work/scripts/$library" git -C "$work" add -- "scripts/$library" done git -C "$work" add -- scripts/tests/test-entry.sh scripts/library.sh scripts/tests/fixtures/fake.sh -fail() { echo "::error::$*" >&2; exit 1; } -if bash "$guard" "$work" > "$work/out" 2>&1; then fail 'accepted a tracked non-executable test entry'; fi +fail() { + echo "::error::$*" >&2 + exit 1 +} +if bash "$guard" "$work" >"$work/out" 2>&1; then fail 'accepted a tracked non-executable test entry'; fi grep -Fq 'scripts/tests/test-entry.sh' "$work/out" || fail 'did not identify the offending entry' chmod +x "$work/scripts/tests/test-entry.sh" git -C "$work" update-index --chmod=+x scripts/tests/test-entry.sh bash "$guard" "$work" cp "$work/.mega-linter.yml" "$work/valid-linter.yml" yq -i '.BASH_EXEC_FILTER_REGEX_EXCLUDE = ".*"' "$work/.mega-linter.yml" -if bash "$guard" "$work" > "$work/out" 2>&1; then fail 'accepted a broadened bash-exec library exclusion'; fi +if bash "$guard" "$work" >"$work/out" 2>&1; then fail 'accepted a broadened bash-exec library exclusion'; fi grep -Fq 'exclude only the four reviewed sourced libraries' "$work/out" || fail 'did not identify the broadened boundary' cp "$work/valid-linter.yml" "$work/.mega-linter.yml" -printf '\n---\nmalformed: [\n' >> "$work/.mega-linter.yml" +printf '\n---\nmalformed: [\n' >>"$work/.mega-linter.yml" parser_status=0 -bash "$guard" "$work" > "$work/out" 2>&1 || parser_status=$? +bash "$guard" "$work" >"$work/out" 2>&1 || parser_status=$? [[ "$parser_status" == 2 ]] || fail 'accepted a partial YAML read or did not fail closed' grep -Fq 'cannot read the bash-exec library exclusion' "$work/out" || fail 'did not identify the partial YAML read' cp "$work/valid-linter.yml" "$work/.mega-linter.yml" git -C "$work" update-index --chmod=+x scripts/ghcr-auth-lib.sh -if bash "$guard" "$work" > "$work/out" 2>&1; then fail 'accepted an executable sourced library'; fi +if bash "$guard" "$work" >"$work/out" 2>&1; then fail 'accepted an executable sourced library'; fi grep -Fq 'scripts/ghcr-auth-lib.sh' "$work/out" || fail 'did not identify the library mode regression' git -C "$work" update-index --chmod=-x scripts/ghcr-auth-lib.sh git -C "$work" update-index --chmod=-x scripts/tests/test-entry.sh [[ -x "$work/scripts/tests/test-entry.sh" ]] || fail 'negative control lost its on-disk execute bit' -if bash "$guard" "$work" > "$work/out" 2>&1; then fail 'accepted a dropped index execute bit hidden by the filesystem'; fi +if bash "$guard" "$work" >"$work/out" 2>&1; then fail 'accepted a dropped index execute bit hidden by the filesystem'; fi git -C "$work" update-index --force-remove -- scripts/tests/test-entry.sh -if bash "$guard" "$work" > "$work/out" 2>&1; then fail 'reported success without any test entries'; fi +if bash "$guard" "$work" >"$work/out" 2>&1; then fail 'reported success without any test entries'; fi printf 'Test entry execute-bit regression passed.\n' diff --git a/scripts/tests/test-guard-tracked-exec-bit.sh b/scripts/tests/test-guard-tracked-exec-bit.sh index 151e2af2ee..f1b9da7cc1 100755 --- a/scripts/tests/test-guard-tracked-exec-bit.sh +++ b/scripts/tests/test-guard-tracked-exec-bit.sh @@ -32,19 +32,19 @@ make_fixture() { git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > "$target" - printf 'jobs:\n j:\n steps:\n - run: %s\n' "$invocation" > .github/workflows/w.yaml + printf '#!/usr/bin/env bash\necho hi\n' >"$target" + printf 'jobs:\n j:\n steps:\n - run: %s\n' "$invocation" >.github/workflows/w.yaml if [[ "$anchor" == "anchor" ]]; then - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh - printf ' - run: ./scripts/fixture-anchor.sh\n' >> .github/workflows/w.yaml + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh + printf ' - run: ./scripts/fixture-anchor.sh\n' >>.github/workflows/w.yaml fi git add -A git update-index --chmod="$mode" "$target" [[ "$anchor" == "anchor" ]] && git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } expect() { @@ -102,19 +102,19 @@ make_workdir_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho hi\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n' printf ' - working-directory: %s\n' "$workdir" printf ' run: %s\n' "$invocation" printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_workdir_fixture "$work/workdir" scripts './fixture-target.sh' expect "WORKDIR relative invocation from a working-directory fails closed" 1 \ @@ -143,7 +143,6 @@ make_workdir_fixture "$work/workdir-untracked" scripts './not-a-tracked-script.s expect "WORKDIR relative path naming no tracked script is out of scope" 0 \ "exec-bit guard OK" "$work/workdir-untracked" - # .GITHUB/SCRIPTS COVERAGE: the CI installers live under .github/scripts and are # invoked as the command of a run step, with no ./ in front. That surface was # outside both the mode inventory and the detector, so a mode-only regression on @@ -170,19 +169,19 @@ make_continuation_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho lib\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho lib\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n - run: |\n' printf ' shellcheck \\\n' printf ' scripts/fixture-target.sh\n' printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_continuation_fixture "$work/continuation" expect "CONT continued argument is not an invocation" 0 "exec-bit guard OK" "$work/continuation" @@ -197,15 +196,15 @@ make_runblock_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > .github/scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho hi\n' >.github/scripts/fixture-target.sh { printf 'jobs:\n j:\n steps:\n - run: |\n' printf ' .github/scripts/fixture-target.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod="$mode" .github/scripts/fixture-target.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_runblock_fixture "$work/runblock" -x expect "BLOCK run-block command 100644 is rejected" 1 "is tracked 100644, not 100755" "$work/runblock" @@ -223,18 +222,18 @@ make_filter_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho lib\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho lib\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'on:\n pull_request:\n paths:\n' printf " - 'scripts/fixture-target.sh'\n" printf 'jobs:\n j:\n steps:\n - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_filter_fixture "$work/filter" expect "FILTER quoted path-filter entry is not an invocation" 0 "exec-bit guard OK" "$work/filter" @@ -340,18 +339,18 @@ make_unquoted_filter_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho lib\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho lib\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'on:\n pull_request:\n paths:\n' printf ' - scripts/fixture-target.sh\n' printf 'jobs:\n j:\n steps:\n - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_unquoted_filter_fixture "$work/filter-unquoted" expect "FILTER unquoted path-filter entry is not an invocation" 0 "exec-bit guard OK" "$work/filter-unquoted" @@ -372,18 +371,18 @@ make_trailarg_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho hi\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n - run: |\n' printf ' scripts/fixture-target.sh --some-flag\n' printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_trailarg_fixture "$work/trailarg" expect "TRAIL run-block bare path with trailing argument is rejected" 1 "is tracked 100644, not 100755" "$work/trailarg" @@ -403,7 +402,6 @@ make_fixture "$work/quoted-assign-bash" -x 'FOO="${BAR}" bash scripts/fixture-ta expect "QUOTED-ASSIGN interpreter behind a quoted assignment stays accepted" 0 \ "" "$work/quoted-assign-bash" - # CHAINED COMMAND: `cmd && ./scripts/x.sh` puts a SECOND command position after the # separator, and the walk has to notice. Extraction is greedy, so the occurrence # begins at `echo`; the classifier hit that unknown token, broke immediately and @@ -434,21 +432,21 @@ make_assign_value_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho hi\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf '#!/usr/bin/env bash\n' printf 'HELPER="./scripts/fixture-target.sh"\n' # shellcheck disable=SC2016 # the literal $HELPER is the fixture; it must not expand printf 'bash "$HELPER"\n' - } > scripts/fixture-caller.sh - printf 'jobs:\n j:\n steps:\n - run: ./scripts/fixture-anchor.sh\n' > .github/workflows/w.yaml + } >scripts/fixture-caller.sh + printf 'jobs:\n j:\n steps:\n - run: ./scripts/fixture-anchor.sh\n' >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git update-index --chmod=+x scripts/fixture-caller.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_assign_value_fixture "$work/assign-value" expect "ASSIGN-VALUE a path stored in a quoted assignment is not an invocation" 0 \ @@ -467,20 +465,20 @@ make_chain_first_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho a\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho b\n' > scripts/fixture-other.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho a\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho b\n' >scripts/fixture-other.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n' printf ' - run: ./scripts/fixture-target.sh && bash ./scripts/fixture-other.sh\n' printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=-x scripts/fixture-other.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_chain_first_fixture "$work/chain-first" expect "CHAIN-FIRST a direct invocation BEFORE a separator is still checked" 1 \ @@ -511,18 +509,18 @@ make_semi_block_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho hi\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n - run: |\n' printf ' echo ready; scripts/fixture-target.sh\n' printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_semi_block_fixture "$work/semi-block" expect "SEMI glued semicolon inside a run block is rejected" 1 \ @@ -597,20 +595,20 @@ make_two_script_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho a\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho b\n' > scripts/fixture-other.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho a\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho b\n' >scripts/fixture-other.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n' printf ' - run: %s\n' "$invocation" printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=-x scripts/fixture-other.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_two_script_fixture "$work/semi-first" './scripts/fixture-target.sh; bash ./scripts/fixture-other.sh' expect "SEMI-FIRST a direct invocation BEFORE a semicolon is still checked" 1 \ @@ -632,19 +630,19 @@ make_multiline_string_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho hi\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n - run: |\n' printf ' echo "multi\n' printf ' %s\n' "$closing_line" printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_multiline_string_fixture "$work/unpaired-semi" 'line"; ./scripts/fixture-target.sh' expect "UNPAIRED a direct invocation after a string's closing quote and ; is rejected" 1 \ @@ -702,19 +700,19 @@ make_argpath_block_fixture() { git init -q . git config user.email t@example.com git config user.name t - printf '#!/usr/bin/env bash\necho hi\n' > scripts/fixture-target.sh - printf '#!/usr/bin/env bash\necho anchor\n' > scripts/fixture-anchor.sh + printf '#!/usr/bin/env bash\necho hi\n' >scripts/fixture-target.sh + printf '#!/usr/bin/env bash\necho anchor\n' >scripts/fixture-anchor.sh { printf 'jobs:\n j:\n steps:\n - run: |\n' printf ' %s\n' "$first" [[ -z "$second" ]] || printf ' %s\n' "$second" printf ' - run: ./scripts/fixture-anchor.sh\n' - } > .github/workflows/w.yaml + } >.github/workflows/w.yaml git add -A git update-index --chmod=-x scripts/fixture-target.sh git update-index --chmod=+x scripts/fixture-anchor.sh git -c commit.gpgsign=false commit -qm fixture - cd - > /dev/null + cd - >/dev/null } make_argpath_block_fixture "$work/argpath-block" 'scripts/fixture-target.sh scripts/fixture-anchor.sh' '' expect "ARGPATH a run-block command with a script path argument is rejected" 1 \ diff --git a/scripts/tests/test-hcloud-baseline-worker-affinity.sh b/scripts/tests/test-hcloud-baseline-worker-affinity.sh index 38588d3085..d8afe75172 100755 --- a/scripts/tests/test-hcloud-baseline-worker-affinity.sh +++ b/scripts/tests/test-hcloud-baseline-worker-affinity.sh @@ -30,7 +30,10 @@ work="$(mktemp -d)" trap 'rm -rf "${work}"' EXIT kubectl kustomize "${infra}" >"${work}/infra.yaml" -[ -s "${work}/infra.yaml" ] || { echo "::error::infrastructure overlay rendered nothing"; exit 1; } +[ -s "${work}/infra.yaml" ] || { + echo "::error::infrastructure overlay rendered nothing" + exit 1 +} fail=0 checked=0 @@ -90,7 +93,10 @@ check_affinity "vault-snapshot-init Job" "${work}/init.yaml" # Coroot: every spec object with hcloud storage, found by walking the CR. yq ea 'select(.kind == "Coroot" and .metadata.name == "coroot") | .spec' "${work}/infra.yaml" >"${work}/coroot.yaml" -[ -s "${work}/coroot.yaml" ] || { echo "::error::Coroot CR did not render"; fail=1; } +[ -s "${work}/coroot.yaml" ] || { + echo "::error::Coroot CR did not render" + fail=1 +} coroot_paths=() # The root spec itself has no path segment; yq prints an empty line for it. while IFS= read -r p; do @@ -113,7 +119,10 @@ for p in "${coroot_paths[@]}"; do done for c in clickhouse clickhouse.keeper; do anti="$(yq ".${c}.affinity.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution // [] | length" "${work}/coroot.yaml")" - [ "${anti}" -ge 1 ] || { echo "::error::coroot ${c} lost the base's required podAntiAffinity"; fail=1; } + [ "${anti}" -ge 1 ] || { + echo "::error::coroot ${c} lost the base's required podAntiAffinity" + fail=1 + } done # OpenBao: requires the static workers BY HOSTNAME, not by the label. It deploys @@ -127,7 +136,10 @@ done # must equal ksail.prod.yaml's metadata.name, which is what KSail names nodes by. controllers="${repo_root}/k8s/providers/hetzner/infrastructure/controllers" kubectl kustomize "${controllers}" >"${work}/controllers.yaml" -[ -s "${work}/controllers.yaml" ] || { echo "::error::infrastructure-controllers overlay rendered nothing"; exit 1; } +[ -s "${work}/controllers.yaml" ] || { + echo "::error::infrastructure-controllers overlay rendered nothing" + exit 1 +} workers="$(yq '.spec.cluster.workers' "${repo_root}/ksail.prod.yaml")" case "${workers}" in '' | 0 | *[!0-9]*) @@ -160,7 +172,10 @@ else checked=$((checked + 1)) fi anti="$(yq '[.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution // [] | .[] | select(.topologyKey == "kubernetes.io/hostname")] | length' "${work}/openbao-affinity.yaml")" - [ "${anti}" -ge 1 ] || { echo "::error::OpenBao lost the chart's required hostname podAntiAffinity"; fail=1; } + [ "${anti}" -ge 1 ] || { + echo "::error::OpenBao lost the chart's required hostname podAntiAffinity" + fail=1 + } fi if [ "${fail}" -ne 0 ]; then diff --git a/scripts/tests/test-hubble-ui-streaming.sh b/scripts/tests/test-hubble-ui-streaming.sh index 26d8106c80..3f9632af93 100755 --- a/scripts/tests/test-hubble-ui-streaming.sh +++ b/scripts/tests/test-hubble-ui-streaming.sh @@ -2,11 +2,18 @@ set -euo pipefail mode="${1:-}" -case "$mode" in --render-only|--runtime) ;; *) printf 'usage: %s --render-only|--runtime\n' "$0" >&2; exit 2 ;; esac +case "$mode" in --render-only | --runtime) ;; *) + printf 'usage: %s --render-only|--runtime\n' "$0" >&2 + exit 2 + ;; +esac root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" scratch="$(mktemp -d)" trap 'rm -rf "$scratch"' EXIT -fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} helm_bin="${CONTROLLER_HELM:?use the audited controller Helm renderer}" pull_bin="${HELM_PULL_BIN:-helm}" release="$root_dir/k8s/bases/infrastructure/controllers/cilium/helm-release.yaml" diff --git a/scripts/tests/test-inventory-first-party-image-signatures.sh b/scripts/tests/test-inventory-first-party-image-signatures.sh index 63ab4b9fa8..d04e186239 100755 --- a/scripts/tests/test-inventory-first-party-image-signatures.sh +++ b/scripts/tests/test-inventory-first-party-image-signatures.sh @@ -105,7 +105,7 @@ identity_subject='^https://github\.com/devantler-tech/business-site/\.github/wor identity_policy="${repo_root}/k8s/bases/infrastructure/cluster-policies/best-practices/verify-app-images.yaml" identity_rules="${repo_root}/talos/cluster/verify-first-party-images.yaml" if [ "$(yq -r '.spec.attestors[] | select(.name == "publishcustomeridentity") | .cosign.keyless.identities[0].subjectRegExp' "$identity_policy")" != "$identity_subject" ] || - [ "$(yq -r '.spec.attestors[] | select(.name == "publishcustomeridentity") | .cosign.keyless.identities[0].issuer' "$identity_policy")" != https://token.actions.githubusercontent.com ]; then + [ "$(yq -r '.spec.attestors[] | select(.name == "publishcustomeridentity") | .cosign.keyless.identities[0].issuer' "$identity_policy")" != https://token.actions.githubusercontent.com ]; then echo 'FAIL dedicated customer identity admission contract is absent or differs' failures=$((failures + 1)) fi diff --git a/scripts/tests/test-kro-renovate-group.sh b/scripts/tests/test-kro-renovate-group.sh index d7c960a243..8d5454fee8 100755 --- a/scripts/tests/test-kro-renovate-group.sh +++ b/scripts/tests/test-kro-renovate-group.sh @@ -48,7 +48,7 @@ managers="$(jq -c --arg path "${oci_path}" ' # The manager's pattern must capture the pin the compiler check reads, and nothing else. [[ "$(jq -r '.[0].matchStrings | length' <<<"${managers}")" == '1' && - "$(jq -r '.[0].matchStrings[0]' <<<"${managers}")" == 'tag:\s*(?\S+)' ]] || +"$(jq -r '.[0].matchStrings[0]' <<<"${managers}")" == 'tag:\s*(?\S+)' ]] || fail 'the KRO chart manager must use the single pattern tag:\s*(?\S+)' pin="$(yq -er '.spec.ref.tag' "${oci_repository}")" || fail "cannot read .spec.ref.tag from ${oci_repository}" matches="$(grep -Eo 'tag:[[:space:]]*[^[:space:]]+' "${oci_repository}" | sed -E 's/^tag:[[:space:]]*//')" || diff --git a/scripts/tests/test-kubelet-config-readback.sh b/scripts/tests/test-kubelet-config-readback.sh index bd091dbf68..90973d5364 100755 --- a/scripts/tests/test-kubelet-config-readback.sh +++ b/scripts/tests/test-kubelet-config-readback.sh @@ -20,7 +20,7 @@ fi stability_line=$(grep -nF './scripts/wait-for-prod-api-stability.sh' "$deploy_action" | cut -d: -f1) readback_line=$(grep -nF './scripts/verify-kubelet-config-readback.sh' "$deploy_action" | cut -d: -f1) -if [[ -z "$stability_line" || -z "$readback_line" ]] || (( stability_line >= readback_line )); then +if [[ -z "$stability_line" || -z "$readback_line" ]] || ((stability_line >= readback_line)); then printf 'kubelet settings readback must follow cluster update and API stability\n' >&2 exit 1 fi diff --git a/scripts/tests/test-kubescape-runtime-rules-switch.sh b/scripts/tests/test-kubescape-runtime-rules-switch.sh index 25c146cd92..9bd98b2eb4 100755 --- a/scripts/tests/test-kubescape-runtime-rules-switch.sh +++ b/scripts/tests/test-kubescape-runtime-rules-switch.sh @@ -24,7 +24,10 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" scratch="$(mktemp -d "${TMPDIR:-/tmp}/kubescape-runtime-rules.XXXXXX")" trap 'rm -rf -- "$scratch"' EXIT -fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} # The declared switch: "true" or "false", or the reason it is not a boolean. declared_switch() { @@ -120,8 +123,7 @@ controller_helm="${CONTROLLER_HELM:-$scratch/controller-helm}" if [[ -z "${CONTROLLER_HELM:-}" ]]; then bash "$root/scripts/build-controller-helm.sh" "$controller_helm" fi -[[ "$("$controller_helm" version --template '{{ printf "%#v" . }}')" == \ - 'version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"go1.26.8", KubeClientVersion:"v1.36"}' ]] || +[[ "$("$controller_helm" version --template '{{ printf "%#v" . }}')" == 'version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"go1.26.8", KubeClientVersion:"v1.36"}' ]] || fail 'the audited controller renderer is required' # Render the chart with one switch state and apply the release's post-renderers as Flux does. @@ -139,7 +141,7 @@ render() { --namespace kubescape --kube-version "$kube_version" --values "$scratch/values.json" \ >"$scratch/rendered.yaml" fi - for ((i=0; i&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} # Every Linux worker is eligible, with or without a storage disk; a Windows node never is, # and no required node affinity or fixed node name narrows that again. @@ -65,8 +68,7 @@ controller_helm="${CONTROLLER_HELM:-$scratch/controller-helm}" if [[ -z "${CONTROLLER_HELM:-}" ]]; then bash "$root/scripts/build-controller-helm.sh" "$controller_helm" fi -[[ "$("$controller_helm" version --template '{{ printf "%#v" . }}')" == \ - 'version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"go1.26.8", KubeClientVersion:"v1.36"}' ]] || +[[ "$("$controller_helm" version --template '{{ printf "%#v" . }}')" == 'version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"go1.26.8", KubeClientVersion:"v1.36"}' ]] || fail 'the audited controller renderer is required' for mode in install upgrade; do @@ -79,7 +81,7 @@ for mode in install upgrade; do yq ea -o=json -I=0 'select(.kind == "Deployment" or .kind == "DaemonSet")' "$scratch/rendered.yaml" | jq -s 'map({kind,name:.metadata.name} + (.spec.template.spec | {nodeSelector,affinity,tolerations,nodeName,topologySpreadConstraints})) | sort_by(.kind,.name)' \ >"$scratch/before-placement.json" - for ((i=0; i&2; exit 1 ;; + *) + echo 'FAIL: unreviewed local API asset platform' >&2 + exit 1 + ;; esac curl --proto '=https' --proto-redir '=https' --location --tlsv1.2 \ --fail --silent --show-error --connect-timeout 10 --max-time 180 \ @@ -52,7 +55,10 @@ if command -v sha256sum >/dev/null 2>&1; then else actual_sha="$(shasum -a 256 "${scratch}/envtest.tar.gz" | cut -d ' ' -f 1)" fi -[[ "${actual_sha}" == "${expected_sha}" ]] || { echo 'FAIL: local API checksum changed' >&2; exit 1; } +[[ "${actual_sha}" == "${expected_sha}" ]] || { + echo 'FAIL: local API checksum changed' >&2 + exit 1 +} tar -xzf "${scratch}/envtest.tar.gz" -C "${scratch}" cd "${root_dir}/scripts/tests/kyverno-auth-reviews/api" KYVERNO_MATCH_CONDITIONS="${scratch}/conditions.json" \ diff --git a/scripts/tests/test-kyverno-background-hotfix.sh b/scripts/tests/test-kyverno-background-hotfix.sh index 4ec46906ee..afe64c7025 100755 --- a/scripts/tests/test-kyverno-background-hotfix.sh +++ b/scripts/tests/test-kyverno-background-hotfix.sh @@ -4,20 +4,23 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" workflow="$root/.github/workflows/publish-kyverno-background-hotfix.yaml" patch="$root/k8s/bases/infrastructure/controllers/kyverno/background-v1.19.2-downstream-cleanup.patch" dockerfile="$root/k8s/bases/infrastructure/controllers/kyverno/BackgroundDownstreamCleanup.Dockerfile" -fail() { printf 'FAIL: background hotfix: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: background hotfix: %s\n' "$1" >&2 + exit 1 +} [[ -f "$workflow" && -f "$patch" && -f "$dockerfile" ]] || fail 'publisher inputs missing' [[ "$(yq -r '.permissions|length' "$workflow")" == 0 ]] || fail 'deny permissions by default' [[ "$(yq -r '.jobs.verify.permissions|length' "$workflow")" == 1 && - "$(yq -r '.jobs.verify.permissions.contents' "$workflow")" == read ]] || fail 'verification must remain read-only' +"$(yq -r '.jobs.verify.permissions.contents' "$workflow")" == read ]] || fail 'verification must remain read-only' [[ "$(yq -r '.jobs.publish.permissions|length' "$workflow")" == 3 && - "$(yq -r '.jobs.publish.permissions.contents' "$workflow")" == read && - "$(yq -r '.jobs.publish.permissions.packages' "$workflow")" == write && - "$(yq -r '.jobs.publish.permissions."id-token"' "$workflow")" == write ]] || fail 'publisher permissions changed' +"$(yq -r '.jobs.publish.permissions.contents' "$workflow")" == read && +"$(yq -r '.jobs.publish.permissions.packages' "$workflow")" == write && +"$(yq -r '.jobs.publish.permissions."id-token"' "$workflow")" == write ]] || fail 'publisher permissions changed' [[ "$(yq -r '.jobs.publish.if' "$workflow")" == "github.event_name == 'push' && github.ref == 'refs/heads/main'" && - "$(yq -r '.jobs.publish.needs' "$workflow")" == verify ]] || fail 'publish only tested main pushes' +"$(yq -r '.jobs.publish.needs' "$workflow")" == verify ]] || fail 'publish only tested main pushes' [[ "$(yq -r '.env.SOURCE_COMMIT' "$workflow")" == e63308f1e16c05cd009ac4864be6ec25c66b49be && - "$(yq -r '.env.PATCHED_SOURCE_TREE' "$workflow")" == 52bd71206733efbd2a2ce86595315c9706ada94a && - "$(yq -r '.env.IMAGE' "$workflow")" == ghcr.io/devantler-tech/platform-kyverno-background ]] || fail 'source or destination changed' +"$(yq -r '.env.PATCHED_SOURCE_TREE' "$workflow")" == 52bd71206733efbd2a2ce86595315c9706ada94a && +"$(yq -r '.env.IMAGE' "$workflow")" == ghcr.io/devantler-tech/platform-kyverno-background ]] || fail 'source or destination changed' # This expression belongs to GitHub, not the shell. # shellcheck disable=SC2016 [[ "$(yq -r '.env.IMAGE_TAG' "$workflow")" == 'v1.19.2-downstream-cleanup.1-${{ github.sha }}' ]] || fail 'immutable source tag missing' diff --git a/scripts/tests/test-kyverno-migration-hook-lifecycle.sh b/scripts/tests/test-kyverno-migration-hook-lifecycle.sh index 9cafadfc18..570a500181 100755 --- a/scripts/tests/test-kyverno-migration-hook-lifecycle.sh +++ b/scripts/tests/test-kyverno-migration-hook-lifecycle.sh @@ -10,7 +10,10 @@ scratch="$(mktemp -d)" readonly root_dir release repository scratch trap 'rm -rf "${scratch}"' EXIT -fail() { printf 'FAIL: kyverno migration hook: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: kyverno migration hook: %s\n' "$1" >&2 + exit 1 +} for tool in helm jq kubectl yq; do command -v "${tool}" >/dev/null || fail "${tool} is required" done diff --git a/scripts/tests/test-kyverno-migration-runtime.sh b/scripts/tests/test-kyverno-migration-runtime.sh index 69e9a24aba..f6281fc50d 100755 --- a/scripts/tests/test-kyverno-migration-runtime.sh +++ b/scripts/tests/test-kyverno-migration-runtime.sh @@ -4,20 +4,23 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" workflow="$root/.github/workflows/publish-kyverno-migration-runtime.yaml" dockerfile="$root/k8s/bases/infrastructure/controllers/kyverno/MigrationRuntime.Dockerfile" patch="$root/k8s/bases/infrastructure/controllers/kyverno/migration-v1.19.2-runtime.patch" -fail() { printf 'FAIL: migration runtime: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: migration runtime: %s\n' "$1" >&2 + exit 1 +} [[ -f "$workflow" && -f "$dockerfile" && -f "$patch" ]] || fail 'publisher inputs missing' [[ "$(yq -r '.permissions|length' "$workflow")" == 0 ]] || fail 'deny permissions by default' [[ "$(yq -r '.jobs.verify.permissions|length' "$workflow")" == 1 && - "$(yq -r '.jobs.verify.permissions.contents' "$workflow")" == read ]] || fail 'verification must remain read-only' +"$(yq -r '.jobs.verify.permissions.contents' "$workflow")" == read ]] || fail 'verification must remain read-only' [[ "$(yq -r '.jobs.publish.permissions|length' "$workflow")" == 3 && - "$(yq -r '.jobs.publish.permissions.contents' "$workflow")" == read && - "$(yq -r '.jobs.publish.permissions.packages' "$workflow")" == write && - "$(yq -r '.jobs.publish.permissions."id-token"' "$workflow")" == write ]] || fail 'publisher permissions changed' +"$(yq -r '.jobs.publish.permissions.contents' "$workflow")" == read && +"$(yq -r '.jobs.publish.permissions.packages' "$workflow")" == write && +"$(yq -r '.jobs.publish.permissions."id-token"' "$workflow")" == write ]] || fail 'publisher permissions changed' [[ "$(yq -r '.jobs.publish.if' "$workflow")" == "github.event_name == 'push' && github.ref == 'refs/heads/main'" && - "$(yq -r '.jobs.publish.needs' "$workflow")" == verify ]] || fail 'only tested main pushes may publish' +"$(yq -r '.jobs.publish.needs' "$workflow")" == verify ]] || fail 'only tested main pushes may publish' [[ "$(yq -r '.env.SOURCE_COMMIT' "$workflow")" == e63308f1e16c05cd009ac4864be6ec25c66b49be && - "$(yq -r '.env.PATCHED_SOURCE_TREE' "$workflow")" == a21cf56e1e85e371753d0a6d4ca3e0a65e235dd2 && - "$(yq -r '.env.IMAGE' "$workflow")" == ghcr.io/devantler-tech/platform-kyverno-migrate ]] || fail 'source or destination changed' +"$(yq -r '.env.PATCHED_SOURCE_TREE' "$workflow")" == a21cf56e1e85e371753d0a6d4ca3e0a65e235dd2 && +"$(yq -r '.env.IMAGE' "$workflow")" == ghcr.io/devantler-tech/platform-kyverno-migrate ]] || fail 'source or destination changed' # shellcheck disable=SC2016 [[ "$(yq -r '.env.IMAGE_TAG' "$workflow")" == 'v1.19.2-migration.1-${{ github.sha }}' ]] || fail 'immutable image tag missing' while IFS= read -r action; do diff --git a/scripts/tests/test-longhorn-client-placement.sh b/scripts/tests/test-longhorn-client-placement.sh index c8538bedcc..5a4ec70341 100755 --- a/scripts/tests/test-longhorn-client-placement.sh +++ b/scripts/tests/test-longhorn-client-placement.sh @@ -14,14 +14,16 @@ cleanup() { fi } trap cleanup EXIT -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} for tool in kubectl helm yq jq; do command -v "${tool}" >/dev/null || fail "${tool} is required" done [[ "$(helm version --template '{{.Version}}')" == v4.2.4 ]] || fail 'the audited Helm v4.2.4 chart puller is required' [[ -n "${CONTROLLER_HELM:-}" && -x "${CONTROLLER_HELM}" ]] || fail 'the audited controller SDK renderer is required' -[[ "$("${CONTROLLER_HELM}" version --template '{{ printf "%#v" . }}')" == \ - 'version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"go1.26.8", KubeClientVersion:"v1.36"}' ]] || +[[ "$("${CONTROLLER_HELM}" version --template '{{ printf "%#v" . }}')" == 'version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"go1.26.8", KubeClientVersion:"v1.36"}' ]] || fail 'controller SDK renderer metadata does not match Flux 2.9.6' kube_version="$(yq -er '.spec.cluster.kubernetesVersion | select(tag == "!!str" and . != "")' "${root}/ksail.prod.yaml")" diff --git a/scripts/tests/test-longhorn-csi-plugin-baseline-context.sh b/scripts/tests/test-longhorn-csi-plugin-baseline-context.sh index 65f8c89191..dbee6bb73f 100755 --- a/scripts/tests/test-longhorn-csi-plugin-baseline-context.sh +++ b/scripts/tests/test-longhorn-csi-plugin-baseline-context.sh @@ -97,7 +97,11 @@ any() { yq -r ea "[select(.kind == \"DaemonSet\" and .metadata.namespace == \"longhorn-system\" and .metadata.name == \"${target}\") | $2] | any" "$1"/*.yaml } -{ cat "${fixtures_dir}/resources.yaml"; printf -- '---\n'; cat "${fixtures_dir}/near-misses.yaml"; } >"${work_dir}/targets.yaml" +{ + cat "${fixtures_dir}/resources.yaml" + printf -- '---\n' + cat "${fixtures_dir}/near-misses.yaml" +} >"${work_dir}/targets.yaml" apply "${work_dir}/targets.yaml" "${work_dir}/live" live="${work_dir}/live" diff --git a/scripts/tests/test-longhorn-engine-image-baseline-context.sh b/scripts/tests/test-longhorn-engine-image-baseline-context.sh index b03c064ef9..2a1289f36e 100755 --- a/scripts/tests/test-longhorn-engine-image-baseline-context.sh +++ b/scripts/tests/test-longhorn-engine-image-baseline-context.sh @@ -98,7 +98,11 @@ any() { yq -r ea "[select(.kind == \"DaemonSet\" and .metadata.namespace == \"longhorn-system\" and .metadata.name == \"${target}\") | $2] | any" "$1"/*.yaml } -{ cat "${fixtures_dir}/resources.yaml"; printf -- '---\n'; cat "${fixtures_dir}/near-misses.yaml"; } >"${work_dir}/targets.yaml" +{ + cat "${fixtures_dir}/resources.yaml" + printf -- '---\n' + cat "${fixtures_dir}/near-misses.yaml" +} >"${work_dir}/targets.yaml" apply "${work_dir}/targets.yaml" "${work_dir}/live" live="${work_dir}/live" diff --git a/scripts/tests/test-longhorn-ui-baseline-context.sh b/scripts/tests/test-longhorn-ui-baseline-context.sh index d7e6fe9595..426d5ff6d2 100755 --- a/scripts/tests/test-longhorn-ui-baseline-context.sh +++ b/scripts/tests/test-longhorn-ui-baseline-context.sh @@ -8,7 +8,10 @@ scratch="$(mktemp -d)" trap 'rm -rf "${scratch}"' EXIT release_dir="${root_dir}/k8s/providers/hetzner/infrastructure/controllers/longhorn" -fail() { printf 'FAIL: %s\n' "$1" >&2; exit 1; } +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} for tool in kubectl helm yq jq; do command -v "${tool}" >/dev/null || fail "${tool} is required" done diff --git a/scripts/tests/test-merge-group-evicted.sh b/scripts/tests/test-merge-group-evicted.sh index 3f2d57a01e..512a12b457 100755 --- a/scripts/tests/test-merge-group-evicted.sh +++ b/scripts/tests/test-merge-group-evicted.sh @@ -15,7 +15,10 @@ trap 'rm -rf "$scratch"' EXIT failures=0 assertions=0 -[ -x "$script" ] || { printf 'FAIL: %s is not executable\n' "$script"; exit 1; } +[ -x "$script" ] || { + printf 'FAIL: %s is not executable\n' "$script" + exit 1 +} sha="$(printf 'c%.0s' {1..40})" queue_ref="refs/heads/gh-readonly-queue/main/pr-3084-$sha" diff --git a/scripts/tests/test-openbao-standby-canary.sh b/scripts/tests/test-openbao-standby-canary.sh index ba9acbc0ff..dd87b566fd 100755 --- a/scripts/tests/test-openbao-standby-canary.sh +++ b/scripts/tests/test-openbao-standby-canary.sh @@ -94,9 +94,9 @@ resolve_replica_placeholder "${scratch}/values.yaml" "${replicas}" # remaining public cluster placeholders exactly as Flux does, without replacing # literal chart values or changing the strategy under test. OPENBAO_STORAGE_PLACEHOLDER="\${openbao_storage_size:=1Gi}" \ -OPENBAO_CANARY_STORAGE="$(yq -er '.data.openbao_storage_size' "${root_dir}/k8s/clusters/prod/bootstrap/config-map.yaml")" \ -OPENBAO_CANARY_CLUSTER="$(yq -er '.data.cluster_name' "${root_dir}/k8s/clusters/prod/bootstrap/config-map.yaml")" \ -yq -i ' + OPENBAO_CANARY_STORAGE="$(yq -er '.data.openbao_storage_size' "${root_dir}/k8s/clusters/prod/bootstrap/config-map.yaml")" \ + OPENBAO_CANARY_CLUSTER="$(yq -er '.data.cluster_name' "${root_dir}/k8s/clusters/prod/bootstrap/config-map.yaml")" \ + yq -i ' (.server.dataStorage.size | select(. == strenv(OPENBAO_STORAGE_PLACEHOLDER))) = strenv(OPENBAO_CANARY_STORAGE) | .server.affinity |= sub("\\$\\{cluster_name\\}", strenv(OPENBAO_CANARY_CLUSTER)) ' "${scratch}/values.yaml" @@ -124,7 +124,7 @@ readonly statefulset="${scratch}/statefulset.yaml" # A clean render alone cannot prove a transition from retained live fields. OPENBAO_TRANSITION_MANIFEST="${statefulset}" \ -OPENBAO_TRANSITION_RELEASE="${release}" \ + OPENBAO_TRANSITION_RELEASE="${release}" \ bash "${root_dir}/scripts/tests/test-openbao-strategy-transition.sh" # A partitioned rolling update recreates a lost lower ordinal from the stored diff --git a/scripts/tests/test-openbao-strategy-transition.sh b/scripts/tests/test-openbao-strategy-transition.sh index 9257849c2c..9ae7533ef5 100755 --- a/scripts/tests/test-openbao-strategy-transition.sh +++ b/scripts/tests/test-openbao-strategy-transition.sh @@ -19,7 +19,10 @@ case "$(uname -s)/$(uname -m)" in asset=envtest-v1.36.2-darwin-arm64.tar.gz expected_sha=f344e7c70961b100471eeea4d2555006f282a6a27bece7f42fbede77b29b886e ;; - *) echo 'FAIL: unreviewed local API asset platform' >&2; exit 1 ;; + *) + echo 'FAIL: unreviewed local API asset platform' >&2 + exit 1 + ;; esac curl --proto '=https' --proto-redir '=https' --location --tlsv1.2 \ --fail --silent --show-error --connect-timeout 10 --max-time 180 \ diff --git a/scripts/tests/test-parallel-guard-cleanup.sh b/scripts/tests/test-parallel-guard-cleanup.sh index ae09eaf5d3..5d92d71adf 100755 --- a/scripts/tests/test-parallel-guard-cleanup.sh +++ b/scripts/tests/test-parallel-guard-cleanup.sh @@ -126,7 +126,10 @@ kill -TERM "$parent" rc=0 wait "$parent" || rc=$? parent='' -[ "$rc" -eq 143 ] || { printf 'cancellation lost its failing exit code: %s\n' "$rc" >&2; exit 1; } +[ "$rc" -eq 143 ] || { + printf 'cancellation lost its failing exit code: %s\n' "$rc" >&2 + exit 1 +} assert_stopped "$state" # Fail actual fixture construction immediately after the first queued worker @@ -187,7 +190,10 @@ wait_for_parent_exit rc=0 wait "$parent" || rc=$? parent='' -[ "$rc" -eq 143 ] || { printf 'witness cancellation lost its exit code: %s\n' "$rc" >&2; exit 1; } +[ "$rc" -eq 143 ] || { + printf 'witness cancellation lost its exit code: %s\n' "$rc" >&2 + exit 1 +} assert_stopped "$state" # A producer that exits before readiness must fail, rather than consuming the @@ -203,7 +209,10 @@ fi rc=0 wait "$parent" || rc=$? parent='' -[ "$rc" -eq 42 ] || { printf 'fixture producer lost its failing exit code\n' >&2; exit 1; } +[ "$rc" -eq 42 ] || { + printf 'fixture producer lost its failing exit code\n' >&2 + exit 1 +} grep -qF 'fixture producer exited before witness readiness' "$state/output" || { printf 'fixture exit was mistaken for a readiness timeout\n' >&2 cat "$state/output" >&2 diff --git a/scripts/tests/test-private-mail-configuration.sh b/scripts/tests/test-private-mail-configuration.sh index 1ed7c99359..253b183378 100755 --- a/scripts/tests/test-private-mail-configuration.sh +++ b/scripts/tests/test-private-mail-configuration.sh @@ -7,21 +7,26 @@ assert_diagnostic_log_privacy() { local log=$1 secret shift if ! grep -Fq 'server.startup' "$log"; then - echo 'FAIL: normal serving produced no startup diagnostic event' >&2; return 1 + echo 'FAIL: normal serving produced no startup diagnostic event' >&2 + return 1 fi for secret in "$@"; do if grep -Fq -f "$secret" "$log"; then - echo 'FAIL: a generated fixture credential appeared in server logs' >&2; return 1 + echo 'FAIL: a generated fixture credential appeared in server logs' >&2 + return 1 fi done if grep -Eqi 'temporary administrator|private-mail-recovery-fixture-only' "$log"; then - echo 'FAIL: a recovery credential appeared in server logs' >&2; return 1 + echo 'FAIL: a recovery credential appeared in server logs' >&2 + return 1 fi if grep -Eqi '(^|[^[:alnum:]_-])(smtp|imap|jmap|auth|oauth|message|message-ingest|queue|delivery|http|websocket)\.[[:alnum:]_-]+' "$log"; then - echo 'FAIL: excluded message or authentication events appeared in server logs' >&2; return 1 + echo 'FAIL: excluded message or authentication events appeared in server logs' >&2 + return 1 fi if grep -Fq -e 'Native policy proof' -e 'mail-policy@example.test' -e 'Preserved native mail fixture.' -e 'Human submission proof' -e 'Receiving continuity' -e 'Native client retention proof' -e 'private-canary' "$log"; then - echo 'FAIL: private fixture message content appeared in server logs' >&2; return 1 + echo 'FAIL: private fixture message content appeared in server logs' >&2 + return 1 fi } test_diagnostic_log_privacy() ( @@ -48,7 +53,10 @@ test_diagnostic_log_privacy if [ "${1:-}" = --test-log-privacy ]; then exit 0; fi root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" fixture_user="$(id -u):$(id -g)" -[ "$(id -u)" != 0 ] || { echo 'FAIL: native mail proof requires an unprivileged fixture user' >&2; exit 1; } +[ "$(id -u)" != 0 ] || { + echo 'FAIL: native mail proof requires an unprivileged fixture user' >&2 + exit 1 +} scratch="$(mktemp -d)" name="mail-config-$(basename "$scratch" | tr '[:upper:]' '[:lower:]')" network="${name}-network" @@ -60,11 +68,11 @@ cleanup() { local rc=$? cleanup_rc=0 trap - EXIT if [ "$proxy_started" = 1 ]; then - docker rm -fv "$proxy_name" > /dev/null 2>&1 || cleanup_rc=1 + docker rm -fv "$proxy_name" >/dev/null 2>&1 || cleanup_rc=1 fi - docker rm -fv "$name" > /dev/null 2>&1 || cleanup_rc=1 - docker network rm "$network" > /dev/null 2>&1 || cleanup_rc=1 - docker image rm "$name-bootstrap-image" > /dev/null 2>&1 || cleanup_rc=1 + docker rm -fv "$name" >/dev/null 2>&1 || cleanup_rc=1 + docker network rm "$network" >/dev/null 2>&1 || cleanup_rc=1 + docker image rm "$name-bootstrap-image" >/dev/null 2>&1 || cleanup_rc=1 rm -rf "$scratch" if [ "$rc" = 0 ] && [ "$cleanup_rc" != 0 ]; then echo 'FAIL: native mail proof cleanup was not confirmed' >&2 @@ -102,7 +110,8 @@ docker run --rm --network none --read-only --user "$fixture_user" --cpus 1 --mem --entrypoint /reader-proof.test "$name-bootstrap-image" \ -test.run '^TestNativeRecoveryReaderSecurity$' -test.timeout 30s [ "$(docker image inspect "$name-bootstrap-image" --format '{{.Config.User}}')" = '2000:2000' ] || { - echo 'FAIL: bootstrap image lost its unprivileged deployment identity' >&2; exit 1; + echo 'FAIL: bootstrap image lost its unprivileged deployment identity' >&2 + exit 1 } docker run --rm --network none --read-only --user 2000:2000 --cap-drop ALL \ --security-opt no-new-privileges:true --entrypoint /bin/sh "$name-bootstrap-image" \ @@ -173,7 +182,7 @@ start_server() { --mount "type=bind,source=$data_dir,target=/var/lib/stalwart" \ --mount "type=bind,source=$scratch/tmp,target=/tmp" \ "${recovery[@]}" "${application_assets[@]}" --entrypoint /usr/local/bin/stalwart "$name-bootstrap-image" --config /etc/stalwart/config.json >/dev/null - for ((attempt=0;attempt<60;attempt++)); do + for ((attempt = 0; attempt < 60; attempt++)); do if docker exec "$name" curl --fail --silent --max-time 2 "http://127.0.0.1:$health_port/healthz/ready" >/dev/null 2>&1; then return 0 fi @@ -270,17 +279,26 @@ verify_rendered_self_checks() { check_command=() while IFS= read -r -d '' arg; do check_command+=("$arg"); done < <(jq -j --arg probe "$probe" '.[$probe].exec.command[] | ., "\u0000"' "$scratch/health-container.json") docker exec "$target" "${check_command[@]}" >"$scratch/$probe.stdout" 2>"$scratch/$probe.stderr" - [ ! -s "$scratch/$probe.stdout" ] || { echo 'FAIL: a self-check printed response content' >&2; exit 1; } + [ ! -s "$scratch/$probe.stdout" ] || { + echo 'FAIL: a self-check printed response content' >&2 + exit 1 + } refused_command=("${check_command[@]}") rewritten=0 for i in "${!refused_command[@]}"; do refused_command[i]="${refused_command[i]//$loopback/127.0.0.1:1}" - if [[ "${refused_command[i]}" != "${check_command[i]}" ]]; then rewritten=$((rewritten+1)); fi + if [[ "${refused_command[i]}" != "${check_command[i]}" ]]; then rewritten=$((rewritten + 1)); fi done - [ "$rewritten" = 1 ] || { echo 'FAIL: self-check lacks its unique loopback target' >&2; exit 1; } + [ "$rewritten" = 1 ] || { + echo 'FAIL: self-check lacks its unique loopback target' >&2 + exit 1 + } status=0 docker exec "$target" "${refused_command[@]}" >"$scratch/$probe-refused.stdout" 2>"$scratch/$probe-refused.stderr" || status=$? - [ "$status" = 7 ] || { echo 'FAIL: the native connection-refusal control was not observed' >&2; exit 1; } + [ "$status" = 7 ] || { + echo 'FAIL: the native connection-refusal control was not observed' >&2 + exit 1 + } docker exec "$target" "${check_command[@]}" >"$scratch/$probe-recovered.stdout" 2>"$scratch/$probe-recovered.stderr" done echo 'PASS: actual rendered self-checks accept native health and reject an unreachable endpoint' @@ -324,7 +342,11 @@ printf '%s' '{"aliases":{}}' >"$scratch/clear-owned-aliases.json" for collision in user-primary group-primary user-alias disabled-user-alias list-primary list-alias; do object=Account response_index=10 - case "$collision" in list-*) object=MailingList; response_index=12 ;; esac + case "$collision" in list-*) + object=MailingList + response_index=12 + ;; + esac native_cli update Account "$maintainer_id" --file /proof/clear-owned-aliases.json >"$scratch/clear-owned.out" 2>"$scratch/clear-owned.err" jq -cn --arg kind "$collision" --arg object "$object" --arg domain "$domain_id" ' ({name:(if ($kind|endswith("-primary")) then "former-a" else "owned-negative" end),domainId:$domain} @@ -337,13 +359,15 @@ for collision in user-primary group-primary user-alias disabled-user-alias list- negative_id=$(jq -er --argjson i "$response_index" --arg kind "$collision" '.methodResponses[$i][1].list|map(select(.name==(if ($kind|endswith("-primary")) then "former-a" else "owned-negative" end)))|if length==1 then .[0].id else error("ambiguous owned negative") end' "$scratch/inputs/listener-inventory.json") jq -S --argjson i "$response_index" --arg id "$negative_id" '.methodResponses[$i][1].list|map(select(.id==$id))' "$scratch/inputs/listener-inventory.json" >"$scratch/receiving-owner-before.json" if prepare_plan "$scratch/unsafe-receiving.ndjson" >"$scratch/receiving-refusal.log" 2>&1; then - echo 'FAIL: a competing receiving owner produced a plan' >&2; exit 1 + echo 'FAIL: a competing receiving owner produced a plan' >&2 + exit 1 fi [ ! -e "$scratch/unsafe-receiving.ndjson" ] docker stop --time 30 "$name" >/dev/null docker rm "$name" >/dev/null if run_bootstrap; then - echo 'FAIL: bootstrap took a competing receiving address' >&2; exit 1 + echo 'FAIL: bootstrap took a competing receiving address' >&2 + exit 1 fi start_server preparation inventory @@ -363,7 +387,8 @@ native_cli update Account "$maintainer_id" --file /proof/drift-aliases.json >"$s docker stop --time 30 "$name" >/dev/null docker rm "$name" >/dev/null if run_bootstrap; then - echo 'FAIL: bootstrap trusted a stale receiving snapshot' >&2; exit 1 + echo 'FAIL: bootstrap trusted a stale receiving snapshot' >&2 + exit 1 fi start_server preparation inventory @@ -375,13 +400,15 @@ native_cli update Account "$maintainer_id" --file /proof/approved-aliases.json > printf '%s\n' '{"@type":"upsert","object":"NetworkListener","matchOn":["name"],"value":{"negative":{"name":"unexpected","protocol":"smtp","bind":{"0.0.0.0:3456":true},"useTls":false}}}' >"$scratch/negative.ndjson" native_cli apply --file /proof/negative.ndjson --json >"$scratch/negative-apply.jsonl" 2>"$scratch/negative-apply.err" if prepare_plan "$scratch/unsafe-plan.ndjson" >"$scratch/negative-render.log" 2>&1; then - echo 'FAIL: unexpected native listener was accepted' >&2; exit 1 + echo 'FAIL: unexpected native listener was accepted' >&2 + exit 1 fi [ ! -e "$scratch/unsafe-plan.ndjson" ] docker stop --time 30 "$name" >/dev/null docker rm "$name" >/dev/null if run_bootstrap; then - echo 'FAIL: bootstrap permitted serving after unsafe inventory' >&2; exit 1 + echo 'FAIL: bootstrap permitted serving after unsafe inventory' >&2 + exit 1 fi start_server preparation native_cli query NetworkListener --where name=unexpected --fields id --json >"$scratch/negative-listener.jsonl" @@ -393,13 +420,15 @@ jq -s '[.[].id] | if length==1 then . else error("negative listener identity is printf '%s\n' '{"@type":"upsert","object":"Application","matchOn":["description"],"value":{"negative":{"description":"Unexpected fixture application","enabled":false,"resourceUrl":"file:///usr/local/share/stalwart/webui.zip","urlPrefix":{"/unexpected":true}}}}' >"$scratch/negative-application.ndjson" native_cli apply --file /proof/negative-application.ndjson --json >"$scratch/negative-application-apply.jsonl" 2>"$scratch/negative-application-apply.err" if prepare_plan "$scratch/unsafe-application-plan.ndjson" >"$scratch/negative-application-render.log" 2>&1; then - echo 'FAIL: unexpected native application was accepted' >&2; exit 1 + echo 'FAIL: unexpected native application was accepted' >&2 + exit 1 fi [ ! -e "$scratch/unsafe-application-plan.ndjson" ] docker stop --time 30 "$name" >/dev/null docker rm "$name" >/dev/null if run_bootstrap; then - echo 'FAIL: bootstrap permitted serving after an unexpected application' >&2; exit 1 + echo 'FAIL: bootstrap permitted serving after an unexpected application' >&2 + exit 1 fi start_server preparation inventory @@ -426,7 +455,8 @@ domain_id=$(jq -er '[.methodResponses[6][1].list[]|select(.name=="example.test") printf '%s' '{"aliases":{"example.invalid":true}}' >"$scratch/sink-alias-patch.json" native_cli update Domain "$domain_id" --file /proof/sink-alias-patch.json >"$scratch/sink-alias.out" 2>"$scratch/sink-alias.err" if prepare_plan "$scratch/unsafe-sink-plan.ndjson" >"$scratch/sink-negative.log" 2>&1; then - echo 'FAIL: a registered rejection-domain alias was accepted' >&2; exit 1 + echo 'FAIL: a registered rejection-domain alias was accepted' >&2 + exit 1 fi [ ! -e "$scratch/unsafe-sink-plan.ndjson" ] printf '%s' '{"aliases":{}}' >"$scratch/restore-alias-patch.json" @@ -521,21 +551,25 @@ auth_advertised() { protocol_curl --fail "${smtp[@]}" --config /proof/agent.curl --mail-rcpt maintainer@example.test >"$scratch/allowed.out" 2>"$scratch/allowed.err" for recipient in operator@example.test someone@remote.example.test maintainer+tag@example.test former-a@example.test former-b@example.test; do if protocol_curl "${smtp[@]}" --config /proof/agent.curl --mail-rcpt "$recipient" >"$scratch/denied.out" 2>"$scratch/denied.err"; then - echo 'FAIL: agent recipient restriction was bypassed' >&2; exit 1 + echo 'FAIL: agent recipient restriction was bypassed' >&2 + exit 1 fi grep -q '550' "$scratch/denied.err" done if protocol_curl --verbose "${smtp[@]}" --mail-rcpt maintainer@example.test >"$scratch/no-auth.out" 2>"$scratch/no-auth.err"; then - echo 'FAIL: submission accepted an unauthenticated envelope' >&2; exit 1 + echo 'FAIL: submission accepted an unauthenticated envelope' >&2 + exit 1 fi grep -q '503' "$scratch/no-auth.err" # The same detector must see AUTH on submission. This probe supplies no # credentials, so capability logging cannot record an authentication secret. auth_advertised "$scratch/no-auth.err" || { - echo 'FAIL: authenticated-submission capabilities were not observed' >&2; exit 1; + echo 'FAIL: authenticated-submission capabilities were not observed' >&2 + exit 1 } if protocol_curl --silent --show-error --max-time 10 --url "smtp://$name:1025" --mail-from sender@remote.example.test --mail-rcpt someone@other.example.test --upload-file /proof/known.eml >"$scratch/relay.out" 2>"$scratch/relay.err"; then - echo 'FAIL: unauthenticated relay was accepted' >&2; exit 1 + echo 'FAIL: unauthenticated relay was accepted' >&2 + exit 1 fi grep -q '550' "$scratch/relay.err" # Human submission retains normal external sending. The isolated .test @@ -553,10 +587,12 @@ protocol_curl --fail --silent --show-error --verbose --max-time 10 \ --ssl-reqd --url smtp://mail.example.test:1025 --mail-from sender@remote.example.test \ --mail-rcpt maintainer@example.test --upload-file /proof/known.eml >"$scratch/mx-auth.out" 2>"$scratch/mx-capabilities.log" if [ "$(grep -c '^> EHLO ' "$scratch/mx-capabilities.log")" -lt 2 ]; then - echo 'FAIL: MX capabilities were not observed before and after TLS' >&2; exit 1 + echo 'FAIL: MX capabilities were not observed before and after TLS' >&2 + exit 1 fi if auth_advertised "$scratch/mx-capabilities.log"; then - echo 'FAIL: MX offered agent authentication' >&2; exit 1 + echo 'FAIL: MX offered agent authentication' >&2 + exit 1 fi for alias in former-a former-b; do printf 'From: sender@remote.example.test\r\nTo: %s@example.test\r\nSubject: Receiving continuity %s\r\nMessage-ID: <%s-receiving@example.test>\r\n\r\nReceiving continuity %s proof.\r\n' "$alias" "$alias" "$alias" "$alias" >"$scratch/alias.eml" @@ -569,7 +605,8 @@ for alias in former-a former-b; do cp "$scratch/$alias-receiving-metadata.json" "$scratch/$alias-receiving-before.json" done if protocol_curl --silent --show-error --max-time 10 --url "smtp://$name:1025" --mail-from sender@remote.example.test --mail-rcpt unknown@example.test --upload-file /proof/known.eml >"$scratch/unknown-recipient.out" 2>"$scratch/unknown-recipient.err"; then - echo 'FAIL: an undeclared receiving address was accepted' >&2; exit 1 + echo 'FAIL: an undeclared receiving address was accepted' >&2 + exit 1 fi grep -q '550' "$scratch/unknown-recipient.err" protocol_curl --fail --silent --show-error --max-time 10 --config /proof/agent.curl "http://$name:8080/jmap/session" >"$scratch/session.json" @@ -624,12 +661,14 @@ if [ "$recovery_mail_status" = 200 ]; then "$scratch/recovery-mail-result.json" >/dev/null; then echo 'EMAIL_RECOVERY_PROTOCOL=MAILBOX_UNAVAILABLE mode=recovery' else - echo 'FAIL: native recovery mailbox protocol returned an unclassified response' >&2; exit 1 + echo 'FAIL: native recovery mailbox protocol returned an unclassified response' >&2 + exit 1 fi elif [ "$recovery_mail_status" = 401 ] || [ "$recovery_mail_status" = 403 ]; then echo 'EMAIL_RECOVERY_PROTOCOL=MAILBOX_UNAVAILABLE mode=recovery' else - echo 'FAIL: native recovery mailbox protocol returned an unexpected status' >&2; exit 1 + echo 'FAIL: native recovery mailbox protocol returned an unexpected status' >&2 + exit 1 fi docker stop --time 30 "$name" >/dev/null docker rm "$name" >/dev/null @@ -708,7 +747,8 @@ jq -e '.methodResponses[0][0]=="Email/query"' "$scratch/expiry-restored.json" >/ echo 'PASS: native client credential and primary expiration survive complete initializer re-entry' protocol_curl --fail --silent --show-error --max-time 10 "http://$name:8080/admin/login" >"$scratch/refreshed-interface.html" if grep -q 'previous-ui-cache-canary' "$scratch/refreshed-interface.html"; then - echo 'FAIL: bootstrap reused the previous cached administration bundle' >&2; exit 1 + echo 'FAIL: bootstrap reused the previous cached administration bundle' >&2 + exit 1 fi verify_administration_assets protocol_curl --fail --silent --show-error --max-time 10 --config /proof/administrator.curl \ @@ -741,7 +781,8 @@ for log in "$scratch/smtp-auth-server.log" "$scratch/server.log"; do assert_diagnostic_log_privacy "$log" "$scratch/inputs/"*-password "$scratch/old-maintainer-password" "$scratch/client-password" done if grep -Eqi 'temporary administrator|private-mail-recovery-fixture-only' "$scratch/server.log"; then - echo 'FAIL: a recovery credential appeared in server logs' >&2; exit 1 + echo 'FAIL: a recovery credential appeared in server logs' >&2 + exit 1 fi # The new transport policy is opt-in. Exercise native persisted readback in # both directions without changing the fixture's identities or mailbox data. @@ -752,7 +793,8 @@ for forwarded in true false; do "$scratch/inputs/settings.json" >"$scratch/inputs/settings.next" mv "$scratch/inputs/settings.next" "$scratch/inputs/settings.json" if [ "$forwarded" = true ] && run_bootstrap --proxy-client-ip=true; then - echo 'FAIL: bootstrap combined HTTP and transport identity modes' >&2; exit 1 + echo 'FAIL: bootstrap combined HTTP and transport identity modes' >&2 + exit 1 fi run_bootstrap start_server serving @@ -787,10 +829,16 @@ for proxy_enabled in true false; do direct_status=0 docker exec "$name" /usr/bin/curl --disable --noproxy '*' --silent \ --max-time 3 http://127.0.0.1:18081/healthz/ready >"$scratch/proxy-direct.out" 2>"$scratch/proxy-direct.err" || direct_status=$? - [ "$direct_status" != 0 ] || { echo 'FAIL: native proxy listener accepted a request without its transport envelope' >&2; exit 1; } + [ "$direct_status" != 0 ] || { + echo 'FAIL: native proxy listener accepted a request without its transport envelope' >&2 + exit 1 + } yq -r '.configMapGenerator[0].literals[0] | sub("^Caddyfile=", "")' "$root/k8s/bases/apps/private-mail-proxy-identity/kustomization.yaml" >"$scratch/ClientProxy.Caddyfile" - [ -s "$scratch/ClientProxy.Caddyfile" ] || { echo 'FAIL: the client proxy configuration could not be read' >&2; exit 1; } + [ -s "$scratch/ClientProxy.Caddyfile" ] || { + echo 'FAIL: the client proxy configuration could not be read' >&2 + exit 1 + } proxy_started=1 docker run --detach --name "$proxy_name" --network "container:$name" --read-only \ --user "$fixture_user" --cpus 0.1 --memory 64m --cap-drop ALL \ @@ -799,24 +847,31 @@ for proxy_enabled in true false; do --entrypoint /usr/local/bin/caddy "$name-bootstrap-image" \ run --config /etc/email-client-proxy/Caddyfile --adapter caddyfile >"$scratch/proxy-start.out" 2>"$scratch/proxy-start.err" proxy_ready=0 - for ((attempt=0; attempt<30; attempt++)); do + for ((attempt = 0; attempt < 30; attempt++)); do if docker exec "$proxy_name" /usr/bin/curl --disable --noproxy '*' --fail --silent \ --max-time 2 --header 'X-Forwarded-For: 127.0.0.1' http://127.0.0.1:8081/healthz/ready >/dev/null 2>&1; then - proxy_ready=1; break + proxy_ready=1 + break fi sleep 1 done - [ "$proxy_ready" = 1 ] || { echo 'FAIL: native proxy-to-mail health did not become ready' >&2; exit 1; } + [ "$proxy_ready" = 1 ] || { + echo 'FAIL: native proxy-to-mail health did not become ready' >&2 + exit 1 + } verify_rendered_self_checks private-mail-proxy-identity client-proxy "$proxy_name" 127.0.0.1:8081 for client_ip in 198.51.100.9 198.51.100.10 2001:db8::9 2001:db8::10 10.20.30.40 fd00::9; do expected=200 - case "$client_ip" in 198.51.100.9|2001:db8::9) expected=502 ;; esac + case "$client_ip" in 198.51.100.9 | 2001:db8::9) expected=502 ;; esac status=$(protocol_curl --silent --show-error --max-time 5 --output /dev/null --write-out '%{http_code}' \ --header "X-Forwarded-For: $client_ip" --header 'Forwarded: for=203.0.113.22' \ --header 'X-Real-IP: 203.0.113.23' --header 'X-Email-Private-Canary: email-proxy-private-canary' \ "http://$name:8081/healthz/ready?private=email-proxy-private-canary") - [ "$status" = "$expected" ] || { echo 'FAIL: native mail ban or per-client identity did not survive proxying' >&2; exit 1; } + [ "$status" = "$expected" ] || { + echo 'FAIL: native mail ban or per-client identity did not survive proxying' >&2 + exit 1 + } done for malformed in missing duplicate chain invalid invalid-ipv6; do header_args=() @@ -828,14 +883,18 @@ for proxy_enabled in true false; do esac status=$(protocol_curl --silent --show-error --max-time 5 --output /dev/null --write-out '%{http_code}' \ "${header_args[@]}" "http://$name:8081/healthz/ready") - [ "$status" = 400 ] || { echo 'FAIL: native proxy accepted missing or ambiguous identity metadata' >&2; exit 1; } + [ "$status" = 400 ] || { + echo 'FAIL: native proxy accepted missing or ambiguous identity metadata' >&2 + exit 1 + } done protocol_curl --fail --silent --show-error --max-time 5 --config /proof/administrator.curl \ --header 'X-Forwarded-For: 198.51.100.10' "http://$name:8081/jmap/session" >"$scratch/proxy-administrator-session.json" jq -e '.accounts|type=="object" and length>0' "$scratch/proxy-administrator-session.json" >/dev/null docker logs "$proxy_name" >"$scratch/proxy-runtime.log" 2>&1 if grep -Fq 'email-proxy-private-canary' "$scratch/proxy-runtime.log"; then - echo 'FAIL: private request metadata reached the proxy log' >&2; exit 1 + echo 'FAIL: private request metadata reached the proxy log' >&2 + exit 1 fi administration_cli query AllowedIp --fields id,address --json >"$scratch/allowed-after.jsonl" jq -sS 'sort_by(.id)' "$scratch/allowed-after.jsonl" >"$scratch/allowed-after.json" diff --git a/scripts/tests/test-private-mail-server.sh b/scripts/tests/test-private-mail-server.sh index bed748faf7..a1dc477cfd 100755 --- a/scripts/tests/test-private-mail-server.sh +++ b/scripts/tests/test-private-mail-server.sh @@ -12,7 +12,7 @@ cleanup() { trap - EXIT # --force tolerates a server that has already exited. A failed removal is # never reported as a successful proof; the runner must surface the residue. - docker rm -fv "$name" "$negative" "${mount_name}-initContainers" "${mount_name}-containers" > /dev/null 2>&1 || cleanup_rc=1 + docker rm -fv "$name" "$negative" "${mount_name}-initContainers" "${mount_name}-containers" >/dev/null 2>&1 || cleanup_rc=1 rm -rf "$scratch" if [ "$cleanup_rc" != 0 ] && [ "$rc" = 0 ]; then echo 'FAIL: mail proof container cleanup was not confirmed' >&2 @@ -87,15 +87,18 @@ docker pull "$image" >/dev/null administration_image="$(yq -er 'select(.kind == "StatefulSet").spec.template.spec.containers[0].image' "$scratch/private-mail-administration.yaml")" mkdir "$scratch/tls" chmod 755 "$scratch/tls" -printf 'disposable mount fixture\n' > "$scratch/tls/tls.crt" -printf 'disposable mount fixture\n' > "$scratch/tls/tls.key" +printf 'disposable mount fixture\n' >"$scratch/tls/tls.crt" +printf 'disposable mount fixture\n' >"$scratch/tls/tls.key" chmod 644 "$scratch/tls/"* docker pull "$administration_image" >/dev/null for group in initContainers containers; do datastore_path="$(GROUP="$group" yq -er 'select(.kind == "StatefulSet").spec.template.spec[strenv(GROUP)][0].volumeMounts[] | select(.name == "datastore").mountPath' "$scratch/private-mail-administration.yaml")" datastore_key="$(GROUP="$group" yq -er 'select(.kind == "StatefulSet").spec.template.spec[strenv(GROUP)][0].volumeMounts[] | select(.name == "datastore").subPath' "$scratch/private-mail-administration.yaml")" tls_path="$(GROUP="$group" yq -er 'select(.kind == "StatefulSet").spec.template.spec[strenv(GROUP)][0].volumeMounts[] | select(.name == "tls").mountPath' "$scratch/private-mail-administration.yaml")" - [[ "$datastore_key" == config.json ]] || { echo 'FAIL: the projected datastore key is unavailable' >&2; exit 1; } + [[ "$datastore_key" == config.json ]] || { + echo 'FAIL: the projected datastore key is unavailable' >&2 + exit 1 + } docker run --name "${mount_name}-${group}" --network none --read-only \ --user 2000:2000 --cpus 1 --memory 512m \ --cap-drop ALL --cap-add NET_BIND_SERVICE \ @@ -103,8 +106,8 @@ for group in initContainers containers; do --mount "type=bind,source=$scratch/config.json,target=$datastore_path,readonly" \ --mount "type=bind,source=$scratch/tls,target=$tls_path,readonly" \ --entrypoint /bin/sh "$administration_image" -ec \ - 'test -r /etc/stalwart/config.json; test -r /etc/stalwart/tls/tls.crt; test -r /etc/stalwart/tls/tls.key; exec /usr/local/bin/stalwart --version' \ - > "$scratch/$group-mount.log" 2>&1 + 'test -r /etc/stalwart/config.json; test -r /etc/stalwart/tls/tls.crt; test -r /etc/stalwart/tls/tls.key; exec /usr/local/bin/stalwart --version' \ + >"$scratch/$group-mount.log" 2>&1 done printf '%s\n' 'PASS: native administration configuration and TLS file mounts' @@ -146,12 +149,18 @@ jq -e '.methodResponses | length == 1 and .[0][0] == "x:DataStore/get"' "$scratc status="$(docker exec "$name" curl --silent --max-time 5 --output /dev/null --write-out '%{http_code}' \ --user fixture:wrong-fixture-password --header 'Content-Type: application/json' \ --data '{"using":["urn:ietf:params:jmap:core"],"methodCalls":[]}' http://127.0.0.1:8080/jmap)" -[ "$status" = 401 ] || { echo 'FAIL: management accepted an incorrect fixture credential' >&2; exit 1; } +[ "$status" = 401 ] || { + echo 'FAIL: management accepted an incorrect fixture credential' >&2 + exit 1 +} for port in 25 143 465 587 993; do rc=0 docker exec "$name" curl --silent --max-time 2 "http://127.0.0.1:$port" >/dev/null 2>&1 || rc=$? - [ "$rc" = 7 ] || { echo 'FAIL: a mail listener was open or its absence was unproven' >&2; exit 1; } + [ "$rc" = 7 ] || { + echo 'FAIL: a mail listener was open or its absence was unproven' >&2 + exit 1 + } done docker logs "$name" >"$scratch/server.log" 2>&1 if grep -Eqi 'temporary administrator|private-mail-fixture-only' "$scratch/server.log"; then diff --git a/scripts/tests/test-prod-recovery-source-wiring.sh b/scripts/tests/test-prod-recovery-source-wiring.sh index eb244109d2..504319c0de 100755 --- a/scripts/tests/test-prod-recovery-source-wiring.sh +++ b/scripts/tests/test-prod-recovery-source-wiring.sh @@ -15,39 +15,108 @@ violation() { cd="$root/.github/workflows/cd.yaml" dr="$root/.github/workflows/dr-rebuild.yaml" [[ "$(yq -r '.inputs.recovery-source-sha.default' "$deploy")" == '' && - "$(yq -r '.inputs.recovery-source-sha.default' "$publisher")" == '' ]] || { echo speculative-default; return; } - [[ "$(yq -r '.runs.steps[0].run' "$deploy")" == './scripts/guard-publish-workflow-approved-revisions.sh' ]] || { echo approved-policy-first; return; } - [[ "$(yq -r '.runs.steps[1].id' "$deploy")" == verify_recovery_source ]] || { echo deploy-first-gate; return; } - [[ "$(yq -r '.runs.steps[1].run' "$deploy")" == "$guard" ]] || { echo deploy-command; return; } + "$(yq -r '.inputs.recovery-source-sha.default' "$publisher")" == '' ]] || { + echo speculative-default + return + } + [[ "$(yq -r '.runs.steps[0].run' "$deploy")" == './scripts/guard-publish-workflow-approved-revisions.sh' ]] || { + echo approved-policy-first + return + } + [[ "$(yq -r '.runs.steps[1].id' "$deploy")" == verify_recovery_source ]] || { + echo deploy-first-gate + return + } + [[ "$(yq -r '.runs.steps[1].run' "$deploy")" == "$guard" ]] || { + echo deploy-command + return + } [[ "$(yq -r '.runs.steps[1] | has("if")' "$deploy")" == false && - "$(yq -r '.runs.steps[1] | has("continue-on-error")' "$deploy")" == false ]] || { echo deploy-unconditional; return; } - [[ "$(yq -r '.runs.steps[1].env.RECOVERY_SOURCE_SHA' "$deploy")" == "\${{ inputs.recovery-source-sha }}" ]] || { echo deploy-binding; return; } - [[ "$(yq -r '.runs.steps[1].env.GH_TOKEN' "$deploy")" == "\${{ github.token }}" ]] || { echo reader-token; return; } - [[ "$(yq -r '.runs.steps[] | select(.id == "publish_platform_manifest") | .with.recovery-source-sha' "$deploy")" == "\${{ inputs.recovery-source-sha }}" ]] || { echo publisher-binding; return; } - [[ "$(yq -r '.jobs.heal-prod-on-failure.steps[] | select(.uses == "./.github/actions/deploy-prod") | .with.recovery-source-sha' "$ci")" == "\${{ steps.recovery-baseline.outputs.sha }}" ]] || { echo heal-binding; return; } - [[ "$(yq -r '.jobs.deploy-prod.steps[] | select(.uses == "./.github/actions/deploy-prod") | .with.recovery-source-sha // ""' "$ci")" == '' ]] || { echo speculative-publication; return; } - [[ "$(yq -r '.jobs.deploy-prod.steps[] | select(.uses == "./.github/actions/deploy-prod") | .with.recovery-source-sha' "$cd")" == "\${{ github.sha }}" ]] || { echo manual-binding; return; } - [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "publish_platform_manifest") | .with.recovery-source-sha' "$dr")" == "\${{ github.sha }}" ]] || { echo rebuild-binding; return; } + "$(yq -r '.runs.steps[1] | has("continue-on-error")' "$deploy")" == false ]] || { + echo deploy-unconditional + return + } + [[ "$(yq -r '.runs.steps[1].env.RECOVERY_SOURCE_SHA' "$deploy")" == "\${{ inputs.recovery-source-sha }}" ]] || { + echo deploy-binding + return + } + [[ "$(yq -r '.runs.steps[1].env.GH_TOKEN' "$deploy")" == "\${{ github.token }}" ]] || { + echo reader-token + return + } + [[ "$(yq -r '.runs.steps[] | select(.id == "publish_platform_manifest") | .with.recovery-source-sha' "$deploy")" == "\${{ inputs.recovery-source-sha }}" ]] || { + echo publisher-binding + return + } + [[ "$(yq -r '.jobs.heal-prod-on-failure.steps[] | select(.uses == "./.github/actions/deploy-prod") | .with.recovery-source-sha' "$ci")" == "\${{ steps.recovery-baseline.outputs.sha }}" ]] || { + echo heal-binding + return + } + [[ "$(yq -r '.jobs.deploy-prod.steps[] | select(.uses == "./.github/actions/deploy-prod") | .with.recovery-source-sha // ""' "$ci")" == '' ]] || { + echo speculative-publication + return + } + [[ "$(yq -r '.jobs.deploy-prod.steps[] | select(.uses == "./.github/actions/deploy-prod") | .with.recovery-source-sha' "$cd")" == "\${{ github.sha }}" ]] || { + echo manual-binding + return + } + [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "publish_platform_manifest") | .with.recovery-source-sha' "$dr")" == "\${{ github.sha }}" ]] || { + echo rebuild-binding + return + } first="$(yq -r '.jobs.rebuild.steps | to_entries | map(select(.value.id == "verify_recovery_source")) | .[0].key // -1' "$dr")" create="$(yq -r '.jobs.rebuild.steps | to_entries | map(select(.value.run == "./scripts/run-ksail-prod-with-pull-auth.sh cluster create")) | .[0].key // -1' "$dr")" - [[ "$first" -ge 0 && "$create" -gt "$first" ]] || { echo rebuild-before-create; return; } - [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | .run' "$dr")" == "bash scripts/verify-prod-recovery-source.sh \"\${RECOVERY_SOURCE_SHA}\"" ]] || { echo rebuild-command; return; } - [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | .env.RECOVERY_SOURCE_SHA' "$dr")" == "\${{ github.sha }}" ]] || { echo rebuild-source; return; } - [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | .env.GH_TOKEN' "$dr")" == "\${{ github.token }}" ]] || { echo rebuild-reader; return; } + [[ "$first" -ge 0 && "$create" -gt "$first" ]] || { + echo rebuild-before-create + return + } + [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | .run' "$dr")" == "bash scripts/verify-prod-recovery-source.sh \"\${RECOVERY_SOURCE_SHA}\"" ]] || { + echo rebuild-command + return + } + [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | .env.RECOVERY_SOURCE_SHA' "$dr")" == "\${{ github.sha }}" ]] || { + echo rebuild-source + return + } + [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | .env.GH_TOKEN' "$dr")" == "\${{ github.token }}" ]] || { + echo rebuild-reader + return + } [[ "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | has("if")' "$dr")" == false && - "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | has("continue-on-error")' "$dr")" == false ]] || { echo rebuild-unconditional; return; } - [[ "$(yq -r '.runs.steps[] | select(.id == "promote_latest") | .env.RECOVERY_SOURCE_SHA' "$publisher")" == "\${{ inputs.recovery-source-sha }}" ]] || { echo promotion-binding; return; } - [[ "$(yq -r '.runs.steps[] | select(.id == "promote_latest") | .env.GH_TOKEN' "$publisher")" == "\${{ github.token }}" ]] || { echo promotion-token; return; } + "$(yq -r '.jobs.rebuild.steps[] | select(.id == "verify_recovery_source") | has("continue-on-error")' "$dr")" == false ]] || { + echo rebuild-unconditional + return + } + [[ "$(yq -r '.runs.steps[] | select(.id == "promote_latest") | .env.RECOVERY_SOURCE_SHA' "$publisher")" == "\${{ inputs.recovery-source-sha }}" ]] || { + echo promotion-binding + return + } + [[ "$(yq -r '.runs.steps[] | select(.id == "promote_latest") | .env.GH_TOKEN' "$publisher")" == "\${{ github.token }}" ]] || { + echo promotion-token + return + } promote="$(yq -r '.runs.steps[] | select(.id == "promote_latest") | .run' "$publisher")" [[ "$promote" == "set -euo pipefail $guard -"* ]] || { echo late-guard; return; } +"* ]] || { + echo late-guard + return + } ci_runs="$(yq -r '.jobs."validate-repository-guards".steps[].run // ""' "$ci")" - grep -Fxq 'bash scripts/tests/test-prod-recovery-source.sh' <<< "$ci_runs" || { echo behavioral-ci; return; } - grep -Fxq 'bash scripts/tests/test-prod-recovery-source-wiring.sh' <<< "$ci_runs" || { echo wiring-ci; return; } + grep -Fxq 'bash scripts/tests/test-prod-recovery-source.sh' <<<"$ci_runs" || { + echo behavioral-ci + return + } + grep -Fxq 'bash scripts/tests/test-prod-recovery-source-wiring.sh' <<<"$ci_runs" || { + echo wiring-ci + return + } } got="$(violation "$root_dir")" -[[ -z "$got" ]] || { echo "FAIL: recovery wiring violates $got" >&2; exit 1; } +[[ -z "$got" ]] || { + echo "FAIL: recovery wiring violates $got" >&2 + exit 1 +} for file in .github/actions/deploy-prod/action.yml .github/actions/deploy-prod/publish-platform-manifests/action.yml .github/workflows/ci.yaml .github/workflows/cd.yaml .github/workflows/dr-rebuild.yaml; do mkdir -p "$work_dir/base/$(dirname "$file")" cp "$root_dir/$file" "$work_dir/base/$file" @@ -57,7 +126,10 @@ ablate() { cp -R "$work_dir/base" "$copy" yq -i "$mutation" "$copy/$file" got="$(violation "$copy")" - [[ "$got" == "$want" ]] || { echo "FAIL: $want mutation returned ${got:-nothing}" >&2; exit 1; } + [[ "$got" == "$want" ]] || { + echo "FAIL: $want mutation returned ${got:-nothing}" >&2 + exit 1 + } printf 'PASS: %s ablation\n' "$want" } ablate approved-policy-first .github/actions/deploy-prod/action.yml 'del(.runs.steps[0])' diff --git a/scripts/tests/test-prod-recovery-source.sh b/scripts/tests/test-prod-recovery-source.sh index f68a6ab2b4..346317d7d7 100755 --- a/scripts/tests/test-prod-recovery-source.sh +++ b/scripts/tests/test-prod-recovery-source.sh @@ -8,7 +8,7 @@ mkdir -p "$work_dir/bin" "$work_dir/checkout" FIXTURE_GIT="$(command -v git)" export FIXTURE_GIT git -C "$work_dir/checkout" init -q -printf 'validated\n' > "$work_dir/checkout/source.txt" +printf 'validated\n' >"$work_dir/checkout/source.txt" git -C "$work_dir/checkout" add source.txt git -C "$work_dir/checkout" -c user.name=Fixture -c user.email=fixture@example.invalid \ -c commit.gpgsign=false commit -qm baseline @@ -17,8 +17,8 @@ git -C "$work_dir/checkout" -c user.name=Fixture -c user.email=fixture@example.i -c commit.gpgsign=false commit --allow-empty -qm newer-main newer="$(git -C "$work_dir/checkout" rev-parse HEAD)" git -C "$work_dir/checkout" checkout -q --detach "$baseline" -printf '%s\n' "$newer" > "$work_dir/main-sha" -cat > "$work_dir/bin/gh" <<'GH' +printf '%s\n' "$newer" >"$work_dir/main-sha" +cat >"$work_dir/bin/gh" <<'GH' #!/usr/bin/env bash set -euo pipefail printf '%s\n' "$*" >> "$FIXTURE_DIR/api-calls" @@ -35,13 +35,13 @@ esac printf '{"ref":"refs/heads/main","object":{"type":"commit","sha":"%s"}}\n' "$sha" exit "${FIXTURE_API_EXIT:-0}" GH -cat > "$work_dir/bin/git" <<'GIT' +cat >"$work_dir/bin/git" <<'GIT' #!/usr/bin/env bash set -euo pipefail if [[ "${FIXTURE_GIT_FAIL_DIFF:-0}" == 1 && " $* " == *' diff '* ]]; then exit 128; fi exec "$FIXTURE_GIT" "$@" GIT -cat > "$work_dir/bin/docker" <<'DOCKER' +cat >"$work_dir/bin/docker" <<'DOCKER' #!/usr/bin/env bash set -euo pipefail case "$3" in @@ -60,9 +60,9 @@ export SUBJECT_NAME=ghcr.io/devantler-tech/platform/manifests STAGING_DIGEST="sha256:$(printf '%064d' 1)" export STAGING_DIGEST GITHUB_OUTPUT="$work_dir/output" yq -r '.runs.steps[] | select(.id == "promote_latest") | .run' \ - "$root_dir/.github/actions/deploy-prod/publish-platform-manifests/action.yml" > "$work_dir/promote.sh" + "$root_dir/.github/actions/deploy-prod/publish-platform-manifests/action.yml" >"$work_dir/promote.sh" cd "$root_dir" -if bash "$work_dir/promote.sh" > "$work_dir/log" 2>&1; then +if bash "$work_dir/promote.sh" >"$work_dir/log" 2>&1; then echo 'FAIL: an outdated recovery checkout promoted the production artifact' >&2 exit 1 fi @@ -77,24 +77,35 @@ grep -q 'PROD_RECOVERY_SOURCE=STALE' "$work_dir/log" || { } printf 'PASS: stale recovery cannot promote production\n' -yq -r '.runs.steps[] | select(.id == "verify_recovery_source") | .run' "$root_dir/.github/actions/deploy-prod/action.yml" > "$work_dir/entry.sh" +yq -r '.runs.steps[] | select(.id == "verify_recovery_source") | .run' "$root_dir/.github/actions/deploy-prod/action.yml" >"$work_dir/entry.sh" entry_status=0 -bash "$work_dir/entry.sh" > "$work_dir/log" 2>&1 || entry_status=$? +bash "$work_dir/entry.sh" >"$work_dir/log" 2>&1 || entry_status=$? if [[ "$entry_status" != 1 ]] || ! grep -q 'PROD_RECOVERY_SOURCE=STALE' "$work_dir/log"; then - echo 'FAIL: outdated recovery reached the deployment entry point' >&2; exit 1 + echo 'FAIL: outdated recovery reached the deployment entry point' >&2 + exit 1 fi printf 'PASS: stale recovery stops before deployment tooling and production changes\n' -printf '%s\n' "$baseline" > "$work_dir/main-sha" +printf '%s\n' "$baseline" >"$work_dir/main-sha" check_promotion() { local want="$1" label="$2" status=0 rm -f "$work_dir/promotions" "$work_dir/output" - bash "$work_dir/promote.sh" > "$work_dir/log" 2>&1 || status=$? - [[ "$status" == "$want" ]] || { echo "FAIL: $label returned $status, wanted $want" >&2; cat "$work_dir/log" >&2; exit 1; } + bash "$work_dir/promote.sh" >"$work_dir/log" 2>&1 || status=$? + [[ "$status" == "$want" ]] || { + echo "FAIL: $label returned $status, wanted $want" >&2 + cat "$work_dir/log" >&2 + exit 1 + } if [[ "$want" == 0 ]]; then - [[ "$(cat "$work_dir/promotions")" == promoted ]] || { echo "FAIL: $label never promoted" >&2; exit 1; } + [[ "$(cat "$work_dir/promotions")" == promoted ]] || { + echo "FAIL: $label never promoted" >&2 + exit 1 + } else - [[ ! -e "$work_dir/promotions" ]] || { echo "FAIL: $label promoted rejected bytes" >&2; exit 1; } + [[ ! -e "$work_dir/promotions" ]] || { + echo "FAIL: $label promoted rejected bytes" >&2 + exit 1 + } fi printf 'PASS: %s\n' "$label" } @@ -102,7 +113,10 @@ check_promotion 0 'unchanged validated main promotes' export GITHUB_SHA="$newer" check_promotion 0 'event SHA cannot replace the recorded recovery checkout' unset GITHUB_SHA -[[ "$(head -1 "$work_dir/api-calls")" == 'api --hostname github.com --method GET repos/devantler-tech/platform/git/ref/heads/main' ]] || { echo 'FAIL: freshness read escaped the fixed main endpoint' >&2; exit 1; } +[[ "$(head -1 "$work_dir/api-calls")" == 'api --hostname github.com --method GET repos/devantler-tech/platform/git/ref/heads/main' ]] || { + echo 'FAIL: freshness read escaped the fixed main endpoint' >&2 + exit 1 +} for mode in malformed empty absent wrong-ref wrong-type duplicate; do export FIXTURE_API_MODE="$mode" check_promotion 2 "incomplete $mode response is UNKNOWN" @@ -115,10 +129,10 @@ export FIXTURE_API_EXIT=0 FIXTURE_API_MODE=change-checkout check_promotion 1 'checkout changed during freshness read is stale' git -C "$work_dir/checkout" checkout -q --detach "$baseline" export FIXTURE_API_MODE=normal -printf 'changed\n' > "$work_dir/checkout/source.txt" +printf 'changed\n' >"$work_dir/checkout/source.txt" check_promotion 1 'dirty validated checkout is stale' git -C "$work_dir/checkout" restore source.txt -printf 'changed\n' > "$work_dir/checkout/source.txt" +printf 'changed\n' >"$work_dir/checkout/source.txt" git -C "$work_dir/checkout" add source.txt check_promotion 1 'staged changes to the validated checkout are stale' git -C "$work_dir/checkout" restore --staged --worktree source.txt @@ -138,5 +152,8 @@ check_promotion 2 'missing bounded reader credential is UNKNOWN' export RECOVERY_SOURCE_SHA='' rm -f "$work_dir/api-calls" check_promotion 0 'speculative publication retains its existing path' -[[ ! -e "$work_dir/api-calls" ]] || { echo 'FAIL: speculative publication performed a main freshness read' >&2; exit 1; } +[[ ! -e "$work_dir/api-calls" ]] || { + echo 'FAIL: speculative publication performed a main freshness read' >&2 + exit 1 +} printf 'PASS: complete recovery freshness behavior\n' diff --git a/scripts/tests/test-prod-stranded-gate.sh b/scripts/tests/test-prod-stranded-gate.sh index aadf758a9a..6a4089ec95 100755 --- a/scripts/tests/test-prod-stranded-gate.sh +++ b/scripts/tests/test-prod-stranded-gate.sh @@ -12,7 +12,10 @@ trap 'rm -rf "$scratch"' EXIT failures=0 assertions=0 -[ -x "$script" ] || { printf 'FAIL: %s is not executable\n' "$script"; exit 1; } +[ -x "$script" ] || { + printf 'FAIL: %s is not executable\n' "$script" + exit 1 +} mkdir -p "$scratch/bin" cat >"$scratch/bin/gh" <<'EOF' diff --git a/scripts/tests/test-prove-cilium-default-deny.sh b/scripts/tests/test-prove-cilium-default-deny.sh index fe2937f09d..c3912ee73b 100755 --- a/scripts/tests/test-prove-cilium-default-deny.sh +++ b/scripts/tests/test-prove-cilium-default-deny.sh @@ -187,13 +187,16 @@ check_case() { local receipt="${state}/temp/cilium-deny-proof-123-1/receipt/result.json" if [[ "$expected" == 0 ]]; then if ! jq -e '.verdict == "PASS" and .cleanup == "PASS" and .tests.dns and .tests.allowed_http and .tests.denied_ingress and .tests.denied_egress and .valid_cnp_count == 3 and .floor_valid_cnp_count == 2 and .floor_tests.pre_floor_http and .floor_tests.allowed_http and .floor_tests.denied_ingress and .floor_tests.denied_egress and .floor_tests.denied_target_http' "$receipt" >/dev/null; then - printf 'FAIL %s: complete proof receipt absent\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: complete proof receipt absent\n' "$name" + failures=$((failures + 1)) fi if ! jq -e '([.probe_bindings[].name] | sort) == ["client-allowed","client-denied","server-allowed","server-denied"] and all(.probe_bindings[]; (.uid | startswith("pod-")) and (.ip_sha256 | test("^[0-9a-f]{64}$")) and (.container_sha256 | test("^[0-9a-f]{64}$")) and (.image_sha256 | test("^[0-9a-f]{64}$")) and .restart_count == 0 and .ready == true) and .tests.denied_target_http' "$receipt" >/dev/null; then - printf 'FAIL %s: healthy stable probe identity receipt absent\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: healthy stable probe identity receipt absent\n' "$name" + failures=$((failures + 1)) fi if grep -Eq '10[.]1[.]0[.]|containerd://' "$receipt"; then - printf 'FAIL %s: receipt exposes private probe IP/container identity\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: receipt exposes private probe IP/container identity\n' "$name" + failures=$((failures + 1)) fi # Read the recorded process boundary to prove all helper-only positive # controls and both floor-only denials were exercised before DNS was added. @@ -212,37 +215,47 @@ check_case() { positive["floor"]>=3 && ingress["floor"]>=3 && egress["floor"]>=3 && target["floor"]>=6 && dns["floor"]==0 && positive["dns"]>=3 && ingress["dns"]>=3 && egress["dns"]>=3 && target["dns"]>=6 && dns["dns"]>=3) }' "${state}/commands"; then - printf 'FAIL %s: causal phase/control sequence absent\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: causal phase/control sequence absent\n' "$name" + failures=$((failures + 1)) fi elif [[ -f "$receipt" ]] && jq -e '.verdict == "PASS" and .cleanup == "PASS"' "$receipt" >/dev/null; then - printf 'FAIL %s: failure produced a green receipt\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: failure produced a green receipt\n' "$name" + failures=$((failures + 1)) fi if [[ "$name" == floor_* ]]; then if ! grep -Eq '^FAIL: floor-only healthy target/admitted HTTP and independent ingress/egress denial did not converge$' "${state}/output" || - ! jq -e '.verdict == "FAIL" and .cleanup == "PASS" and .floor_valid_cnp_count == 0' "$receipt" >/dev/null || - grep -Eq 'apply -f .*allow-dns[.]json$' "${state}/commands"; then - printf 'FAIL %s: witness did not reach and reject the isolated floor before DNS\n' "$name"; failures=$((failures + 1)) + ! jq -e '.verdict == "FAIL" and .cleanup == "PASS" and .floor_valid_cnp_count == 0' "$receipt" >/dev/null || + grep -Eq 'apply -f .*allow-dns[.]json$' "${state}/commands"; then + printf 'FAIL %s: witness did not reach and reject the isolated floor before DNS\n' "$name" + failures=$((failures + 1)) fi elif [[ "$name" == dns_after_floor_error ]]; then if ! grep -Eq '^FAIL: complete healthy target/DNS/admitted/independent ingress and egress denial did not converge$' "${state}/output" || - ! jq -e '.verdict == "FAIL" and .cleanup == "PASS" and .floor_valid_cnp_count == 2 and .floor_tests.denied_ingress and .floor_tests.denied_egress' "$receipt" >/dev/null; then - printf 'FAIL %s: final DNS failure bypassed the proven floor phase\n' "$name"; failures=$((failures + 1)) + ! jq -e '.verdict == "FAIL" and .cleanup == "PASS" and .floor_valid_cnp_count == 2 and .floor_tests.denied_ingress and .floor_tests.denied_egress' "$receipt" >/dev/null; then + printf 'FAIL %s: final DNS failure bypassed the proven floor phase\n' "$name" + failures=$((failures + 1)) fi fi if [[ "$name" == unconfirmed || "$name" == occupied || "$name" == tampered || "$name" == docker_error || "$name" == wrong_ksail ]]; then if [[ -f "${state}/commands" ]] && grep -Eq 'ksail .*cluster (create|delete)|kubectl' "${state}/commands"; then - printf 'FAIL %s: rejected preflight touched a cluster\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: rejected preflight touched a cluster\n' "$name" + failures=$((failures + 1)) fi elif [[ "$name" != cleanup_error && -f "${state}/created" ]]; then - printf 'FAIL %s: owned cluster survived cleanup\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: owned cluster survived cleanup\n' "$name" + failures=$((failures + 1)) fi # Every Kubernetes call must use only the private context/config. Deleting # by ambient context, a changed provider or a whole Docker prune is unsafe. if [[ -f "${state}/commands" ]] && grep -E '^kubectl ' "${state}/commands" | grep -Ev -- '--kubeconfig .*/cilium-deny-proof-123-1/kubeconfig --context kind-deny-proof-123-1' >/dev/null; then - printf 'FAIL %s: ambient Kubernetes call\n' "$name"; failures=$((failures + 1)) + printf 'FAIL %s: ambient Kubernetes call\n' "$name" + failures=$((failures + 1)) fi } check_case success 0 for name in unconfirmed occupied docker_error wrong_ksail tampered wrong_version invalid unknown missing exec_error dns_error baseline_error allow_all refused other_timeout cleanup_error changed target_unhealthy_after target_loopback_error target_uid_changed target_ip_changed client_uid_changed target_restarted probe_list_error probe_baseline_not_ready flux_extra_rules floor_unselected floor_not_enforcing floor_no_effect floor_egress_not_enforcing floor_egress_no_effect dns_after_floor_error; do check_case "$name" 1; done -[[ "$failures" == 0 ]] || { printf '%s failures\n' "$failures"; exit 1; } +[[ "$failures" == 0 ]] || { + printf '%s failures\n' "$failures" + exit 1 +} echo 'PASS: isolated proof refuses incomplete/false traffic evidence and cleans only its owned cluster' diff --git a/scripts/tests/test-publication-subject.sh b/scripts/tests/test-publication-subject.sh index 9824c0598f..5909505e6f 100755 --- a/scripts/tests/test-publication-subject.sh +++ b/scripts/tests/test-publication-subject.sh @@ -9,7 +9,10 @@ failures=0 assertions=0 check() { assertions=$((assertions + 1)) - if "$@"; then echo "ok $*"; else echo "FAIL $*"; failures=$((failures + 1)); fi + if "$@"; then echo "ok $*"; else + echo "FAIL $*" + failures=$((failures + 1)) + fi } # shellcheck disable=SC2016 # A literal Actions expression, never a shell expansion. reference='${{ steps.staging_reference.outputs.subject_name }}' diff --git a/scripts/tests/test-publish-workflow-approved-revisions-guard.sh b/scripts/tests/test-publish-workflow-approved-revisions-guard.sh index 7bd2891e72..5af3d3bfbf 100755 --- a/scripts/tests/test-publish-workflow-approved-revisions-guard.sh +++ b/scripts/tests/test-publish-workflow-approved-revisions-guard.sh @@ -26,7 +26,7 @@ readonly SHA_A='1111111111111111111111111111111111111111' readonly SHA_B='2222222222222222222222222222222222222222' readonly SHA_C='3333333333333333333333333333333333333333' readonly SHA_D='4444444444444444444444444444444444444444' -readonly SHA_E='5555555555555555555555555555555555555555' # release candidate (#3960) +readonly SHA_E='5555555555555555555555555555555555555555' # release candidate (#3960) readonly DIGEST='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' readonly GENERIC_KYVERNO='k8s/bases/infrastructure/cluster-policies/best-practices/verify-app-images.yaml' readonly GENERIC_RGD='k8s/bases/infrastructure/resource-graph-definitions/tenant/resource-graph-definition.yaml' @@ -39,7 +39,10 @@ fail() { } pass() { printf 'ok: %s\n' "$*"; } -[ -x "$GUARD" ] || { fail "guard not executable at $GUARD"; exit 1; } +[ -x "$GUARD" ] || { + fail "guard not executable at $GUARD" + exit 1 +} WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT @@ -65,7 +68,10 @@ if [ -z "$consumers" ]; then exit 1 fi consumer_count="$(printf '%s\n' "$consumers" | grep -c .)" -[ "$consumer_count" -ge 2 ] || { fail "need at least two registered consumers to cross pairs, found $consumer_count"; exit 1; } +[ "$consumer_count" -ge 2 ] || { + fail "need at least two registered consumers to cross pairs, found $consumer_count" + exit 1 +} first_consumer="$(printf '%s\n' "$consumers" | sed -n 1p | cut -f1)" first_workflow="$(printf '%s\n' "$consumers" | sed -n 1p | cut -f2)" second_consumer='aws' @@ -224,7 +230,10 @@ build_tree() { case "$form" in pattern) ref="$PATTERN" ;; pair) ref="$(set_for "$repo" "$workflow")" ;; - *) printf 'build_tree: unknown form %s\n' "$form" >&2; exit 1 ;; + *) + printf 'build_tree: unknown form %s\n' "$form" >&2 + exit 1 + ;; esac write_consumer "$root" "$repo" "$workflow" "$ref" done <<<"$consumers" @@ -246,20 +255,26 @@ expect_pass() { # if out="$(run_guard "$2" "$3")"; then pass "$1" else - fail "$1 — expected exit 0, got a refusal:"; printf '%s\n' "$out" >&2 + fail "$1 — expected exit 0, got a refusal:" + printf '%s\n' "$out" >&2 fi } expect_refusal() { # ... local case_name="$1" root="$2" enforce="$3" out needle shift 3 if out="$(run_guard "$root" "$enforce")"; then - fail "$case_name — expected a refusal, guard exited 0:"; printf '%s\n' "$out" >&2 + fail "$case_name — expected a refusal, guard exited 0:" + printf '%s\n' "$out" >&2 return fi for needle in "$@"; do case "$out" in *"$needle"*) ;; - *) fail "$case_name — refusal does not name '$needle':"; printf '%s\n' "$out" >&2; return ;; + *) + fail "$case_name — refusal does not name '$needle':" + printf '%s\n' "$out" >&2 + return + ;; esac done pass "$case_name" @@ -355,22 +370,27 @@ expect_refusal 'a shared-workflow subject in a second document of a consumer fil # signer == pin: the steady state after a consumer re-releases on the revision it pins. same_set() { # — rewrite the first consumer's row with signer == pin == SHA_B local root="$1" - { head -n1 "$root/scripts/approved.tsv" + { + head -n1 "$root/scripts/approved.tsv" tail -n +2 "$root/scripts/approved.tsv" | awk -F'\t' -v c="$first_consumer" -v s="$SHA_B" 'BEGIN{OFS="\t"} $1 "" == c "" {$5=s} {print}' } >"$root/scripts/approved.tmp" mv "$root/scripts/approved.tmp" "$root/scripts/approved.tsv" } -root="$(build_tree same-single pair)"; same_set "$root" +root="$(build_tree same-single pair)" +same_set "$root" write_consumer "$root" "$first_consumer" "$first_workflow" "$SHA_B" expect_pass 'signer == pin: the single concrete revision is the set and passes' "$root" 1 -root="$(build_tree same-group pair)"; same_set "$root" +root="$(build_tree same-group pair)" +same_set "$root" write_consumer "$root" "$first_consumer" "$first_workflow" "($SHA_B)" expect_pass 'signer == pin: the single revision in a group passes' "$root" 1 -root="$(build_tree same-doubled pair)"; same_set "$root" +root="$(build_tree same-doubled pair)" +same_set "$root" write_consumer "$root" "$first_consumer" "$first_workflow" "($SHA_B|$SHA_B)" expect_refusal 'signer == pin: a doubled alternation is not the canonical set and is refused' \ "$root" 0 "$first_consumer" 'not the generated set' -root="$(build_tree same-foreign pair)"; same_set "$root" +root="$(build_tree same-foreign pair)" +same_set "$root" write_consumer "$root" "$first_consumer" "$first_workflow" "($SHA_B|$SHA_D)" expect_refusal 'signer == pin: an alternation adding a foreign revision is refused' \ "$root" 1 "$first_consumer" "$SHA_D" @@ -521,12 +541,16 @@ else fi root="$(build_tree set-bad-header pattern)" -{ printf 'consumer\tworkflow\n'; tail -n +2 "$root/scripts/approved.tsv"; } >"$root/scripts/approved.tmp" +{ + printf 'consumer\tworkflow\n' + tail -n +2 "$root/scripts/approved.tsv" +} >"$root/scripts/approved.tmp" mv "$root/scripts/approved.tmp" "$root/scripts/approved.tsv" expect_refusal "an approved set whose header is not the generator's is refused" "$root" 0 'header' root="$(build_tree set-workflow-mismatch pair)" -other_workflow='publish-manifests'; [ "$first_workflow" = 'publish-manifests' ] && other_workflow='publish-app' +other_workflow='publish-manifests' +[ "$first_workflow" = 'publish-manifests' ] && other_workflow='publish-app' write_consumer "$root" "$first_consumer" "$other_workflow" "($(signer_for "$first_consumer")|$SHA_B)" expect_refusal 'a consumer whose manifest names a different workflow than its row is refused' \ "$root" 0 "$first_consumer" "$other_workflow" @@ -580,7 +604,8 @@ fi if out="$(env -u PUBLISH_BOOTSTRAP_FILE PUBLISH_CONSUMER_ROOT="$REPO_ROOT" APPROVED_REVISIONS_ENFORCE=1 bash "$GUARD" 2>&1)"; then pass 'real tree: every consumer matcher uses its declared boundary (enforced)' else - fail 'real tree: guard refuses the committed state:'; printf '%s\n' "$out" >&2 + fail 'real tree: guard refuses the committed state:' + printf '%s\n' "$out" >&2 fi if [ "$failures" -ne 0 ]; then diff --git a/scripts/tests/test-publish-workflow-approved-revisions.sh b/scripts/tests/test-publish-workflow-approved-revisions.sh index d5d4d74397..1f4ae0dcd4 100755 --- a/scripts/tests/test-publish-workflow-approved-revisions.sh +++ b/scripts/tests/test-publish-workflow-approved-revisions.sh @@ -25,7 +25,7 @@ readonly REPORT="$REPO_ROOT/scripts/report-publish-workflow-signing-revisions.sh readonly SHA_A='1111111111111111111111111111111111111111' readonly SHA_B='2222222222222222222222222222222222222222' readonly SHA_C='3333333333333333333333333333333333333333' -readonly SHA_R='4444444444444444444444444444444444444444' # release candidate (#3960) +readonly SHA_R='4444444444444444444444444444444444444444' # release candidate (#3960) readonly DIGEST_A='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' failures=0 @@ -127,16 +127,19 @@ run_gen() { # ["$WORK/signer-ok.err")" && [ "$out" = "$SHA_B" ] && [ ! -s "$WORK/signer-ok.err" ]; then pass 'the signer lookup resolves one successful run at the tag commit, silently' else - fail "the signer lookup did not resolve a signed tag (out='${out:-}')"; cat "$WORK/signer-ok.err" >&2 + fail "the signer lookup did not resolve a signed tag (out='${out:-}')" + cat "$WORK/signer-ok.err" >&2 fi # A failed runs read is named as a failed read, never as a missing run. @@ -429,7 +454,8 @@ elif grep -q 'runs read for v9.9.9 failed' "$WORK/signer-fail.err" && ! grep -q 'at_commit=' "$WORK/signer-fail.err"; then pass 'a failed runs read is refused as a failed read' else - fail 'a failed runs read was not named as one'; cat "$WORK/signer-fail.err" >&2 + fail 'a failed runs read was not named as one' + cat "$WORK/signer-fail.err" >&2 fi # A listing without the matching run prints what it DID contain, so a late or incomplete @@ -441,7 +467,8 @@ elif grep -q 'total_count=1 returned=1 for_tag=1 cd=1 at_commit=0 successful=0 s "$WORK/signer-miss.err"; then pass 'a listing without the matching run is refused with its bounded counts' else - fail 'a listing without the matching run did not report its counts'; cat "$WORK/signer-miss.err" >&2 + fail 'a listing without the matching run did not report its counts' + cat "$WORK/signer-miss.err" >&2 fi # The counts track the conclusion too: a run at the right commit that did not succeed. @@ -451,7 +478,8 @@ if bash "$driver" >/dev/null 2>"$WORK/signer-concl.err"; then elif grep -q 'at_commit=1 successful=0 signer_refs=0 conclusions=failure' "$WORK/signer-concl.err"; then pass 'a failed CD run at the tag commit is refused with its conclusion' else - fail 'a failed CD run did not report its conclusion'; cat "$WORK/signer-concl.err" >&2 + fail 'a failed CD run did not report its conclusion' + cat "$WORK/signer-concl.err" >&2 fi # Only an EMPTY listing is read again. A listing that holds runs but not exactly one signer diff --git a/scripts/tests/test-publish-workflow-canonical-approvals.sh b/scripts/tests/test-publish-workflow-canonical-approvals.sh index bbfa6c6b0d..27a95ea38e 100755 --- a/scripts/tests/test-publish-workflow-canonical-approvals.sh +++ b/scripts/tests/test-publish-workflow-canonical-approvals.sh @@ -25,11 +25,11 @@ readonly WRITER="$REPO_ROOT/scripts/write-publish-workflow-matchers.sh" readonly REPORT="$REPO_ROOT/scripts/report-publish-workflow-signing-revisions.sh" readonly PATTERN='[0-9a-f]{40}' -readonly SHA_A='1111111111111111111111111111111111111111' # applied signer -readonly SHA_B='2222222222222222222222222222222222222222' # default-branch pin -readonly SHA_E='5555555555555555555555555555555555555555' # release candidate -readonly SHA_K='6666666666666666666666666666666666666666' # reviewed canonical commit -readonly SHA_X='7777777777777777777777777777777777777777' # a commit nobody approved +readonly SHA_A='1111111111111111111111111111111111111111' # applied signer +readonly SHA_B='2222222222222222222222222222222222222222' # default-branch pin +readonly SHA_E='5555555555555555555555555555555555555555' # release candidate +readonly SHA_K='6666666666666666666666666666666666666666' # reviewed canonical commit +readonly SHA_X='7777777777777777777777777777777777777777' # a commit nobody approved readonly DIGEST='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' readonly CANONICAL_REPOSITORY='devantler-tech/.github' readonly LEGACY_SET="($SHA_A|$SHA_B|$SHA_E)" @@ -58,7 +58,10 @@ export PUBLISH_BOOTSTRAP_FILE="$WORK/bootstrap-none.json" printf '{"schemaVersion":1,"publications":[]}\n' >"$PUBLISH_BOOTSTRAP_FILE" consumers="$("$REPORT" --list-consumers 2>/dev/null | cut -f1,2 || true)" -[ -n "$consumers" ] || { fail '--list-consumers produced nothing; every case below would pass vacuously'; exit 1; } +[ -n "$consumers" ] || { + fail '--list-consumers produced nothing; every case below would pass vacuously' + exit 1 +} # Two manifest consumers and one application consumer, taken from the real registry. manifest_consumer="$(printf '%s\n' "$consumers" | awk -F'\t' '$2 == "publish-manifests" && $1 !~ /^\./ {print $1; exit}')" other_manifest_consumer="$(printf '%s\n' "$consumers" | awk -F'\t' -v skip="$manifest_consumer" '$2 == "publish-manifests" && $1 != skip {print $1; exit}')" @@ -143,19 +146,27 @@ run_in() { #