diff --git a/.betterleaks.toml b/.betterleaks.toml new file mode 100644 index 0000000000..9e7ee10c0f --- /dev/null +++ b/.betterleaks.toml @@ -0,0 +1,32 @@ +# Betterleaks configuration. MegaLinter's REPOSITORY_BETTERLEAKS extends this file, and this file +# 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 + +[[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." +regexes = ['''^[0-9]+:[0-9]+$'''] + +[[rules.allowlists]] +description = "Disposable test accounts: the mail-server tests sign in as `fixture` to a throwaway local container." +regexes = ['''^fixture:[a-z-]+$'''] + +[[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." +regexTarget = "line" +# 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*$'''] 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 e8c2739850..bf6b40e9d5 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 @@ -58,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 @@ -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/renovate.json b/.github/renovate.json index dc3cee2cfd..14f2b097b9 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -249,7 +249,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 }, @@ -393,7 +395,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 73fe72a263..11053febcf 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: @@ -149,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 ffed0c02ef..8928c83940 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: @@ -459,6 +460,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 @@ -2020,7 +2024,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 | @@ -2279,6 +2283,17 @@ 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 and image of every container. Its identical-probe trivy control runs only + # where trivy is installed, which this job is not yet (#5864). + 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 @@ -3557,13 +3572,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/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 c2b5a348ac..a51657beeb 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 @@ -111,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 42dd19cac9..cf4a2c299c 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: @@ -79,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/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..9f48e6452d 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: @@ -17,12 +18,32 @@ on: 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/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 815ffd2f6f..60f354f2dd 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..af9f4c331c 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 @@ -199,20 +204,20 @@ 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 - # 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 e2f396f5de..928f1f6c92 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 be0774b491..1b237ca906 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 4f295880d4..e95e3a8f9b 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. @@ -97,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 @@ -207,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/.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 50ad20631f..91284acf0c 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -1,3 +1,4 @@ +--- # MegaLinter configuration for the platform repository. # # WHY THIS FILE EXISTS @@ -351,7 +352,25 @@ 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 compact JSON captured from a real Stalwart store. +# access_test.go builds its negative controls by replacing exact compact substrings such as +# `"total":1`, so prettier's `"total": 1` layout would fail those controls. +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. +# 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 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 474faa422a..ad350f718b 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 @@ -29,6 +30,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 +87,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 +690,166 @@ 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 where + # trivy is installed; CI does not install it yet (#5864), so there only its shape checks run. + - 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/.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/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 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..ffbc944955 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 +PR #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..bf6b9aeb2d 100644 --- a/docs/tenant-abstraction.md +++ b/docs/tenant-abstraction.md @@ -203,8 +203,9 @@ the same name in other namespaces retain their ordinary admission rules. 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. +Routing, projection and live hold/serve/isolate/recover acceptance remain +under #5247 before exposure; the source-only baseline does not enable an +application. ### Public business-site configuration identity diff --git a/go.mod b/go.mod index 08b88e5f82..f4d58aa9d4 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; untagged fix for GO-2026-5932, move to the first tagged release that contains it ) 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/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/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/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/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/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/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 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..749242aa48 100644 --- a/scripts/alert-publisher/README.md +++ b/scripts/alert-publisher/README.md @@ -86,8 +86,8 @@ 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, +The optional `r2` backend prepares the independent journal needed by #5328 +and #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/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/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/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/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-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/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/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/generate-publish-workflow-approved-revisions.sh b/scripts/generate-publish-workflow-approved-revisions.sh index 3980e83125..8850d463aa 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) @@ -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. @@ -167,14 +174,14 @@ 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 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-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/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/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/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod b/scripts/migrate-longhorn-v2/testdata/retirement-fence-proof/go.mod index a15882b856..ae561d9d0a 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,13 +26,14 @@ 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 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 @@ -44,7 +45,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 +58,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..bd2f6f2d91 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= @@ -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= @@ -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/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/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/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 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/private-mail-config/dockerfile_test.go b/scripts/private-mail-config/dockerfile_test.go new file mode 100644 index 0000000000..84b0c89006 --- /dev/null +++ b/scripts/private-mail-config/dockerfile_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "os" + "regexp" + "strings" + "testing" +) + +var ( + dockerfileAddInstruction = regexp.MustCompile(`(?i)^\s*ADD\s`) + dockerfilePinnedChecksum = regexp.MustCompile(`(^|\s)--checksum=sha256:[0-9a-f]{64}(\s|$)`) +) + +// dockerfileInstructions joins backslash-continued lines, so a flag on a +// continuation line still belongs to its instruction. +func dockerfileInstructions(data string) []string { + var instructions []string + current := "" + for _, line := range strings.Split(data, "\n") { + if current == "" && strings.HasPrefix(strings.TrimSpace(line), "#") { + continue + } + trimmed := strings.TrimRight(line, " \t") + if strings.HasSuffix(trimmed, "\\") { + current += strings.TrimSuffix(trimmed, "\\") + " " + continue + } + instructions = append(instructions, current+trimmed) + current = "" + } + if current != "" { + instructions = append(instructions, current) + } + return instructions +} + +// Checkov cannot scope CKV_DOCKER_4 to one instruction: the skip on the caddy +// LICENSE fetch covers the whole Dockerfile. This test is what still refuses an +// ADD that is not pinned by digest. +func TestEveryDockerfileAddIsChecksumPinned(t *testing.T) { + data, err := os.ReadFile("Dockerfile") + if err != nil { + t.Fatal(err) + } + adds := 0 + for _, instruction := range dockerfileInstructions(string(data)) { + if !dockerfileAddInstruction.MatchString(instruction) { + continue + } + adds++ + if !dockerfilePinnedChecksum.MatchString(instruction) { + t.Errorf("ADD without a pinned --checksum=sha256: digest: %s", strings.TrimSpace(instruction)) + } + } + if adds == 0 { + t.Fatal("found no ADD instruction; if the caddy LICENSE fetch was removed, remove its CKV_DOCKER_4 skip and this test") + } +} + +func TestDockerfileInstructionsKeepContinuedFlags(t *testing.T) { + got := dockerfileInstructions("# comment\nADD \\\n --checksum=sha256:" + strings.Repeat("a", 64) + " \\\n https://x /y\nADD https://z /w\n") + if len(got) != 3 || !dockerfilePinnedChecksum.MatchString(got[0]) || dockerfilePinnedChecksum.MatchString(got[1]) { + t.Fatalf("unexpected instructions: %q", got) + } +} 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-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/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/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-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/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/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/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/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/render-helm-chart/go.mod b/scripts/render-helm-chart/go.mod index a63fed2a36..8d73360c86 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,20 +81,23 @@ 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 - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/net v0.56.0 // 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; untagged fix for GO-2026-5932, move to the first tagged release that contains it + 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..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,82 +272,84 @@ 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= -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= +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= +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= -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= +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 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/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/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/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/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/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/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/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/fixtures/coroot-dns-probe/pod.yaml b/scripts/tests/fixtures/coroot-dns-probe/pod.yaml index 88a391e568..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: @@ -7,6 +8,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/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/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/gateway-receiver-admission/go.mod b/scripts/tests/gateway-receiver-admission/go.mod index f3c1f1e63c..f507953297 100644 --- a/scripts/tests/gateway-receiver-admission/go.mod +++ b/scripts/tests/gateway-receiver-admission/go.mod @@ -26,13 +26,14 @@ 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 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 @@ -57,12 +58,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..bd2f6f2d91 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= @@ -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= @@ -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/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/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/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/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod b/scripts/tests/longhorn-benchmark-delete-guard/testdata/native-proof/go.mod index 0f95b1cbe8..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 @@ -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,13 +26,14 @@ 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 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 @@ -44,7 +45,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 +58,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..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 @@ -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= @@ -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= @@ -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/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/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/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/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/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. 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/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/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/go.mod b/scripts/tests/tenant-webapp-admission/go.mod index f15ca10bb0..cb2b6818f9 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; untagged fix for GO-2026-5932, move to the first tagged release that contains it 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/tenant-webapp-admission/main_test.go b/scripts/tests/tenant-webapp-admission/main_test.go index 004f4805b0..cf89f9743f 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,76 @@ import ( "sigs.k8s.io/yaml" ) +var ( + osvIgnoredID = regexp.MustCompile(`^id = "([A-Z]+-[0-9]{4}-[0-9]+)"$`) + osvIgnoredReason = regexp.MustCompile(`^reason = "[^"\\]+"$`) +) + +// osvIgnoredIDs reads osv-scanner.toml line by line and accepts only comments +// and [[IgnoredVulns]] tables holding exactly one id and one reason. Any other +// line, such as a [[PackageOverrides]] table, a single-quoted id or a trailing +// comment, could make osv-scanner ignore more than the listed IDs, so it fails +// the test instead of being skipped. +func osvIgnoredIDs(t *testing.T, data string) []string { + t.Helper() + var ids []string + idsInTable, reasonsInTable, inTable := 0, 0, false + closeTable := func() { + if inTable && (idsInTable != 1 || reasonsInTable != 1) { + t.Fatalf("osv-scanner.toml: each [[IgnoredVulns]] table needs exactly one id and one reason") + } + idsInTable, reasonsInTable = 0, 0 + } + for number, line := range strings.Split(data, "\n") { + switch { + case line == "" || strings.HasPrefix(line, "#"): + case line == "[[IgnoredVulns]]": + closeTable() + inTable = true + case inTable && osvIgnoredID.MatchString(line): + ids = append(ids, osvIgnoredID.FindStringSubmatch(line)[1]) + idsInTable++ + case inTable && osvIgnoredReason.MatchString(line): + reasonsInTable++ + default: + t.Fatalf("osv-scanner.toml line %d is not a reviewed ignore entry: %q", number+1, line) + } + } + closeTable() + return ids +} + +// 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)...) + } + ignored = osvIgnoredIDs(t, readOptional(t, "osv-scanner.toml")) + 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." 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 1c436db805..03561df10e 100755 --- a/scripts/tests/test-canonical-generator-output.sh +++ b/scripts/tests/test-canonical-generator-output.sh @@ -80,7 +80,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 c225c309fb..23b1f0853a 100755 --- a/scripts/tests/test-canonical-source-upgrade.sh +++ b/scripts/tests/test-canonical-source-upgrade.sh @@ -77,19 +77,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" @@ -109,7 +117,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" @@ -120,7 +129,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" @@ -132,7 +145,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" @@ -140,18 +156,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" @@ -164,23 +189,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" @@ -205,9 +236,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() { :; } @@ -216,7 +249,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" @@ -227,14 +260,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 @@ -250,7 +283,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 @@ -265,7 +301,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 @@ -296,7 +336,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 @@ -307,7 +347,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-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-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-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/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() { #