From 98fc2757aed6d127c11a72ca5cc688d0479f91cf Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:26:37 +0100 Subject: [PATCH 1/5] test(web): allow patched DOMPurify ranges in security floor contract The dependency proposal advances DOMPurify beyond the historical fixed version, but the security test asserts exact manifest equality. Accept explicit patched 3.x caret floors while rejecting vulnerable ranges, prereleases and unreviewed majors; preserve locked-copy validation. Focused security and chat sanitisation tests pass 39/39. Change-source: pulse-maintainer --- .../__tests__/dependencySecurity.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts index 1cd69547c..2cb9a5071 100644 --- a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts +++ b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts @@ -59,6 +59,11 @@ const nanoidIsPatched = (version: string): boolean => { return major >= 5 && atLeast(version, [5, 1, 6]); }; +// Accept maintained 3.x patch/minor upgrades without pinning the manifest to +// one historical fix. Reject broader ranges, prereleases and unreviewed majors. +const dompurifyRangeIsPatched = (range: string): boolean => + /^\^3\.\d+\.\d+$/.test(range) && atLeast(range.slice(1), [3, 4, 13]); + describe('frontend dependency security floors', () => { it('keeps Vitest and its mocker above the redirect-mock file-read floor', () => { // GHSA-82fw-gwwq-j7x9: the maintained 4.x fix starts at 4.1.11. @@ -88,11 +93,20 @@ describe('frontend dependency security floors', () => { } }); + it.each(['^3.4.13', '^3.4.14', '^3.5.0'])('accepts patched DOMPurify range %s', (range) => { + expect(dompurifyRangeIsPatched(range)).toBe(true); + }); + + it.each(['^3.4.12', '^3.3.99', '^4.0.0', '^3.4.13-beta.1', '*', '>=3.4.13', '^3.4.13 || ^2.0.0'])('rejects unsafe or unreviewed DOMPurify range %s', (range) => { + expect(dompurifyRangeIsPatched(range)).toBe(false); + }); + it('keeps DOMPurify above the hook-detachment XSS floor', () => { - expect(manifest.dependencies.dompurify).toBe('^3.4.13'); + expect(dompurifyRangeIsPatched(manifest.dependencies.dompurify)).toBe(true); const versions = lockedVersions('dompurify'); expect(versions).not.toHaveLength(0); for (const version of versions) { + expect(version).not.toContain('-'); expect(atLeast(version, [3, 4, 13]), `dompurify ${version} is vulnerable`).toBe(true); } }); From 9a6a5811a701d475880548468271cb28137432a4 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:28:02 +0100 Subject: [PATCH 2/5] ci: qualify setup-node v7 consumer contracts The standalone setup-node proposal lacked lifecycle and deployment evidence. Preserve Node 24, explicit cache controls and native Windows proof steps while upgrading the immutable action revision; add consumer regression coverage for the removed dummy auth-token assumption. Keep grouped signing and deployment action upgrades separate. Change-source: pulse-maintainer --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/build-release-candidate.yml | 2 +- .github/workflows/canonical-governance.yml | 2 +- .github/workflows/compile-release-payload.yml | 2 +- .github/workflows/create-release.yml | 10 ++--- .github/workflows/release-dry-run.yml | 2 +- .github/workflows/security-scan.yml | 2 +- .github/workflows/test-e2e.yml | 8 ++-- .github/workflows/unified-agent-native.yml | 2 +- .../v6/internal/subsystems/agent-lifecycle.md | 17 ++++++++ .../subsystems/deployment-installability.md | 17 ++++++++ .../internal/subsystems/security-privacy.md | 17 ++++++++ .../installtests/build_release_assets_test.go | 40 +++++++++++++++++++ scripts/installtests/install_ps1_test.go | 3 ++ scripts/tests/test_workflow_trust.py | 25 ++++++++++++ 15 files changed, 135 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 01af79551..09ddb4e8f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -138,7 +138,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: npm diff --git a/.github/workflows/build-release-candidate.yml b/.github/workflows/build-release-candidate.yml index d363639c3..bebaa12c1 100644 --- a/.github/workflows/build-release-candidate.yml +++ b/.github/workflows/build-release-candidate.yml @@ -708,7 +708,7 @@ jobs: cache: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' package-manager-cache: false diff --git a/.github/workflows/canonical-governance.yml b/.github/workflows/canonical-governance.yml index 6a7614541..c606ebaaa 100644 --- a/.github/workflows/canonical-governance.yml +++ b/.github/workflows/canonical-governance.yml @@ -38,7 +38,7 @@ jobs: cache-dependency-path: repos/pulse/go.sum - name: Set up Node.js for frontend governance - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: npm diff --git a/.github/workflows/compile-release-payload.yml b/.github/workflows/compile-release-payload.yml index 9ba5deeed..7c1b219bf 100644 --- a/.github/workflows/compile-release-payload.yml +++ b/.github/workflows/compile-release-payload.yml @@ -65,7 +65,7 @@ jobs: cache: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' package-manager-cache: false diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index c625baf3c..3be410c4d 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -409,7 +409,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' @@ -444,7 +444,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: 'npm' @@ -483,7 +483,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: 'npm' @@ -564,7 +564,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: 'npm' @@ -729,7 +729,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' package-manager-cache: false diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index 1fa476e1a..9faa6263f 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -284,7 +284,7 @@ jobs: --github-annotations - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' package-manager-cache: false diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 0322900b1..b40377503 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -439,7 +439,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index aa11ab784..0ea3c97ff 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -48,7 +48,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: 'npm' @@ -83,7 +83,7 @@ jobs: with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: 'npm' @@ -142,7 +142,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: 'npm' @@ -295,7 +295,7 @@ jobs: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' package-manager-cache: false diff --git a/.github/workflows/unified-agent-native.yml b/.github/workflows/unified-agent-native.yml index 79e72d1c2..b4979d4c5 100644 --- a/.github/workflows/unified-agent-native.yml +++ b/.github/workflows/unified-agent-native.yml @@ -105,7 +105,7 @@ jobs: - name: Set up Node.js for Windows command proof if: ${{ !matrix.unix }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' cache: 'npm' diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 95516b861..ce0689083 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -7971,3 +7971,20 @@ Historical incident archives are explicit reads, not a collector lifecycle. metrics sampling loop. Organization teardown drops the archive reference without saving or deleting recordings. Alert observation and recovery continue through the alert manager and canonical resource timeline. + +### Node setup action v7 compatibility + +Workflow consumers pin `actions/setup-node` to +`820762786026740c76f36085b0efc47a31fe5020` (v7.0.0). The action manifest +retains the Node 24 execution runtime, setup/cache-save entry points and existing +inputs. Its internal ESM/toolkit upgrade does not change the requested Node 24 +application toolchain. Consumers do not supply `registry-url` or depend on the +removed dummy `NODE_AUTH_TOKEN` export. Explicit npm lockfile caching for test +jobs and disabled automatic package-manager caching for privileged release jobs +remain unchanged; no permission, dispatch input or signing backend changes. + +`test_setup_node_upgrade_preserves_consumer_contract` in the existing workflow +trust suite verifies every consumer pin, absence of the affected auth assumptions, +workflow trust controls and the retained native Windows command/lifecycle proof +steps. Native Windows execution remains a hosted check, not a local Linux claim. +This upgrade is independent of the grouped signing/Docker/Tailscale updates. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 00e9e4bd4..41178100d 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -5344,3 +5344,20 @@ binding and lifecycle regressions are in `offline-license-issuer.test.mjs` and `with-offline-entitlements.test.mjs`; the two scoped Chromium provisioning scenarios verify the authenticated API/browser path. These are fixture proofs, not release qualification or evidence that other Organization failures cleared. + +### Node setup action v7 compatibility + +Workflow consumers pin `actions/setup-node` to +`820762786026740c76f36085b0efc47a31fe5020` (v7.0.0). The action manifest +retains the Node 24 execution runtime, setup/cache-save entry points and existing +inputs. Its internal ESM/toolkit upgrade does not change the requested Node 24 +application toolchain. Consumers do not supply `registry-url` or depend on the +removed dummy `NODE_AUTH_TOKEN` export. Explicit npm lockfile caching for test +jobs and disabled automatic package-manager caching for privileged release jobs +remain unchanged; no permission, dispatch input or signing backend changes. + +`test_setup_node_upgrade_preserves_consumer_contract` in the existing workflow +trust suite verifies every consumer pin, absence of the affected auth assumptions, +workflow trust controls and the retained native Windows command/lifecycle proof +steps. Native Windows execution remains a hosted check, not a local Linux claim. +This upgrade is independent of the grouped signing/Docker/Tailscale updates. diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 098764cc8..dc5ce3dd8 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -2830,3 +2830,20 @@ symlink checks and expose no enumeration, sampling or writing capability. Removing the disconnected recorder does not alter alert, action approval or operator authority. An unrelated organization receives no default archive fallback. + +### Node setup action v7 compatibility + +Workflow consumers pin `actions/setup-node` to +`820762786026740c76f36085b0efc47a31fe5020` (v7.0.0). The action manifest +retains the Node 24 execution runtime, setup/cache-save entry points and existing +inputs. Its internal ESM/toolkit upgrade does not change the requested Node 24 +application toolchain. Consumers do not supply `registry-url` or depend on the +removed dummy `NODE_AUTH_TOKEN` export. Explicit npm lockfile caching for test +jobs and disabled automatic package-manager caching for privileged release jobs +remain unchanged; no permission, dispatch input or signing backend changes. + +`test_setup_node_upgrade_preserves_consumer_contract` in the existing workflow +trust suite verifies every consumer pin, absence of the affected auth assumptions, +workflow trust controls and the retained native Windows command/lifecycle proof +steps. Native Windows execution remains a hosted check, not a local Linux claim. +This upgrade is independent of the grouped signing/Docker/Tailscale updates. diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 01f36cb52..8ca59fcef 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -4375,3 +4375,43 @@ func workflowStepBlock(t *testing.T, jobBlock, step string) string { } return jobBlock[start : start+len(" - name: "+step+"\n")+end] } + +// The action's ESM migration must not turn privileged release jobs into cache +// writers or change the application toolchain requested by release consumers. +func TestReleaseNodeSetupKeepsExplicitCacheIsolation(t *testing.T) { + for _, name := range []string{"build-release-candidate.yml", "compile-release-payload.yml", "create-release.yml", "release-dry-run.yml"} { + t.Run(name, func(t *testing.T) { + content, err := os.ReadFile(repoFile(".github", "workflows", name)) + if err != nil { + t.Fatal(err) + } + blocks := regexp.MustCompile(`(?m)^ uses: actions/setup-node@[^\n]+\n(?: [^\n]*\n| with:\n)*`).FindAllString(string(content), -1) + if len(blocks) == 0 { + t.Fatal("missing Node setup") + } + if name == "create-release.yml" { + // First four jobs build/test; only the fifth is the privileged + // release consumer. Preserve the three explicit test caches. + if len(blocks) != 5 { + t.Fatalf("review changed release job layout: %d", len(blocks)) + } + for i, block := range blocks[:4] { + if i > 0 && (!strings.Contains(block, "cache: 'npm'") || !strings.Contains(block, "cache-dependency-path: 'frontend-modern/package-lock.json'")) { + t.Fatalf("test cache lost its lockfile: %s", block) + } + } + blocks = blocks[4:] + } + for _, block := range blocks { + for _, want := range []string{"actions/setup-node@820762786026740c76f36085b0efc47a31fe5020", "node-version: '24'", "package-manager-cache: false"} { + if !strings.Contains(block, want) { + t.Fatalf("Node setup lost %s: %s", want, block) + } + } + if strings.Contains(block, "registry-url:") || strings.Contains(block, " cache:") { + t.Fatalf("unexpected authentication or explicit cache: %s", block) + } + } + }) + } +} diff --git a/scripts/installtests/install_ps1_test.go b/scripts/installtests/install_ps1_test.go index cef10118b..9931fab47 100644 --- a/scripts/installtests/install_ps1_test.go +++ b/scripts/installtests/install_ps1_test.go @@ -136,6 +136,9 @@ func TestNativeWindowsExecutesGeneratedInstallCommand(t *testing.T) { for _, needle := range []string{ `frontend-modern/src/utils/agentInstallCommand.ts`, `Execute generated command with Windows PowerShell 5.1`, + `uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0`, + `node-version: '24'`, + `cache-dependency-path: 'frontend-modern/package-lock.json'`, `agentInstallCommand.windows.test.ts`, } { if !strings.Contains(workflow, needle) { diff --git a/scripts/tests/test_workflow_trust.py b/scripts/tests/test_workflow_trust.py index 3800198dc..aeef6481a 100644 --- a/scripts/tests/test_workflow_trust.py +++ b/scripts/tests/test_workflow_trust.py @@ -36,6 +36,31 @@ class WorkflowTrustTest(unittest.TestCase): path.write_text(content, encoding="utf-8") return [finding.message for finding in workflow_trust.audit_workflow(path)] + def test_setup_node_upgrade_preserves_consumer_contract(self) -> None: + # v7 changes its internal module format, not the Node 24 consumer ABI. + # No consumer may rely on the removed dummy NODE_AUTH_TOKEN export. + count = 0 + for path in (REPO_ROOT / ".github/workflows").glob("*.yml"): + source = path.read_text() + if "actions/setup-node@" not in source: + continue + with self.subTest(workflow=path.name): + self.assertNotIn("registry-url:", source) + self.assertNotIn("NODE_AUTH_TOKEN", source) + for line in source.splitlines(): + if "uses: actions/setup-node@" in line: + count += 1 + self.assertEqual( + line.strip(), + "uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0", + ) + self.assertEqual(workflow_trust.audit_workflow(path), []) + self.assertGreater(count, 0) + native = (REPO_ROOT / ".github/workflows/unified-agent-native.yml").read_text() + self.assertIn("agentInstallCommand.windows.test.ts", native) + self.assertIn("^Test(InstallPS1|WindowsAgentLifecycle)", native) + self.assertIn("node-version: '24'", native) + def test_accepts_immutable_dependencies_and_explicit_checkout_credentials(self) -> None: findings = self.audit( f"""permissions: From e9a13105288ab58e4aa780fcea9b5efafad04042 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 19:30:02 +0100 Subject: [PATCH 3/5] fix(deps): split Kubernetes update with discovery-compatible tests client-go 0.37 extends the Discovery return interface, which broke the metrics test double in grouped dependency PR #1977. Use the real discovery client over an in-memory HTTP transport so the fixture follows the selected client API without losing metrics and error assertions. Upgrade only the coordinated Kubernetes modules and their selected transitive dependencies. Leave the unrelated grouped updates unchanged and check cohort alignment in local runtime orchestration, including mixed and incomplete negative cases. Change-source: pulse-maintainer --- .../subsystems/deployment-installability.md | 13 +++++ go.mod | 37 +++++++++----- go.sum | 48 +++++++++++++++++++ .../usage_metrics_additional_test.go | 45 +++++------------ scripts/tests/test-hot-dev-runtime.sh | 38 +++++++++++++++ 5 files changed, 136 insertions(+), 45 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 00e9e4bd4..663a0f986 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -5344,3 +5344,16 @@ binding and lifecycle regressions are in `offline-license-issuer.test.mjs` and `with-offline-entitlements.test.mjs`; the two scoped Chromium provisioning scenarios verify the authenticated API/browser path. These are fixture proofs, not release qualification or evidence that other Organization failures cleared. + + +### Kubernetes dependency cohort + +The native agent's `k8s.io/api`, `k8s.io/apimachinery` and `k8s.io/client-go` +modules move together at the same release version. Local orchestration checks +resolve the selected graph read-only and reject incomplete or mixed cohorts; +future aligned upgrades need not retain a fixed minor-version pin. Dependency +updates must also run the native Kubernetes agent tests, including real discovery +REST transport coverage of metrics/summary merging and backend unavailability. +The test transport remains in-memory and does not contact a cluster. Version +0.37 compatibility is not installed-cluster acceptance or qualification of other +libraries in a grouped dependency proposal. diff --git a/go.mod b/go.mod index ac23843e5..167fe4987 100644 --- a/go.mod +++ b/go.mod @@ -33,9 +33,9 @@ require ( golang.org/x/sys v0.47.0 golang.org/x/term v0.45.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.36.2 - k8s.io/apimachinery v0.36.2 - k8s.io/client-go v0.36.2 + k8s.io/api v0.37.0 + k8s.io/apimachinery v0.37.0 + k8s.io/client-go v0.37.0 modernc.org/sqlite v1.53.0 ) @@ -53,14 +53,25 @@ require ( github.com/ebitengine/purego v0.10.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - 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/go-openapi/jsonpointer v1.0.0 // indirect + github.com/go-openapi/jsonreference v1.0.0 // indirect + github.com/go-openapi/swag v0.27.1 // indirect + github.com/go-openapi/swag/cmdutils v0.27.1 // indirect + github.com/go-openapi/swag/conv v0.27.1 // indirect + github.com/go-openapi/swag/fileutils v0.27.1 // indirect + github.com/go-openapi/swag/jsonutils v0.27.1 // indirect + github.com/go-openapi/swag/loading v0.27.1 // indirect + github.com/go-openapi/swag/mangling v0.27.1 // indirect + github.com/go-openapi/swag/netutils v0.27.1 // indirect + github.com/go-openapi/swag/pools v0.27.1 // indirect + github.com/go-openapi/swag/stringutils v0.27.1 // indirect + github.com/go-openapi/swag/typeutils v0.27.1 // indirect + github.com/go-openapi/swag/yamlutils v0.27.1 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect @@ -87,7 +98,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/russellhaering/goxmldsig v1.6.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -97,22 +108,22 @@ require ( go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // 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/text v0.41.0 // indirect - golang.org/x/time v0.14.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 gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect - k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect - k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect + k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect modernc.org/libc v1.73.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 321187371..cfb8c80ab 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5 github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -52,11 +54,39 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= +github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= +github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg= +github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE= +github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE= +github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= +github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU= +github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs= +github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM= +github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= +github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU= +github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk= +github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY= +github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE= +github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA= +github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= +github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU= +github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= +github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E= +github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= +github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8= +github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= +github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc= +github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= +github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA= +github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo= github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw= github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= @@ -169,6 +199,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -204,6 +236,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 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/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/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= @@ -228,6 +262,8 @@ 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/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +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.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -249,16 +285,26 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4= +k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk= k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0= +k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE= k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ= +k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= +k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs= +k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= @@ -295,5 +341,7 @@ sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q= +sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/kubernetesagent/usage_metrics_additional_test.go b/internal/kubernetesagent/usage_metrics_additional_test.go index 87882907a..645f46001 100644 --- a/internal/kubernetesagent/usage_metrics_additional_test.go +++ b/internal/kubernetesagent/usage_metrics_additional_test.go @@ -12,31 +12,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" - "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/rest" restfake "k8s.io/client-go/rest/fake" ) -type kubeClientWithDiscovery struct { - kubernetes.Interface - discoveryClient discovery.DiscoveryInterface -} - -func (c *kubeClientWithDiscovery) Discovery() discovery.DiscoveryInterface { - return c.discoveryClient -} - -type discoveryWithREST struct { - discovery.DiscoveryInterface - restClient rest.Interface -} - -func (d *discoveryWithREST) RESTClient() rest.Interface { - return d.restClient -} - func newTestRESTClient(handler func(path string) (int, string)) *restfake.RESTClient { scheme := runtime.NewScheme() codecs := serializer.NewCodecFactory(scheme) @@ -62,17 +42,18 @@ func newTestRESTClient(handler func(path string) (int, string)) *restfake.RESTCl } } -func newTestAgentWithREST(restClient rest.Interface) *Agent { - base := fake.NewSimpleClientset() +// Use the real discovery client over an in-memory HTTP transport rather than +// overriding Discovery: client-go can extend its return interface independently +// of the metrics REST API this fixture exercises. +func newTestAgentWithREST(t *testing.T, restClient *restfake.RESTClient) *Agent { + t.Helper() + client, err := kubernetes.NewForConfigAndClient(&rest.Config{Host: "https://kubernetes.invalid"}, restClient.Client) + if err != nil { + t.Fatalf("create Kubernetes test client: %v", err) + } return &Agent{ - logger: zerolog.New(io.Discard), - kubeClient: &kubeClientWithDiscovery{ - Interface: base, - discoveryClient: &discoveryWithREST{ - DiscoveryInterface: base.Discovery(), - restClient: restClient, - }, - }, + logger: zerolog.New(io.Discard), + kubeClient: client, } } @@ -249,7 +230,7 @@ func TestCollectUsageMetrics_MergesMetricsAndSummary(t *testing.T) { } }) - agent := newTestAgentWithREST(restClient) + agent := newTestAgentWithREST(t, restClient) nodes := []agentsk8s.Node{{Name: "node-a"}} nodeUsage, podUsage, err := agent.collectUsageMetrics(context.Background(), nodes) @@ -284,7 +265,7 @@ func TestCollectUsageMetrics_ReturnsErrorWhenAllBackendsUnavailable(t *testing.T } }) - agent := newTestAgentWithREST(restClient) + agent := newTestAgentWithREST(t, restClient) nodes := []agentsk8s.Node{{Name: "node-a"}} nodeUsage, podUsage, err := agent.collectUsageMetrics(context.Background(), nodes) diff --git a/scripts/tests/test-hot-dev-runtime.sh b/scripts/tests/test-hot-dev-runtime.sh index e262e61c1..6f6fa6dda 100755 --- a/scripts/tests/test-hot-dev-runtime.sh +++ b/scripts/tests/test-hot-dev-runtime.sh @@ -359,6 +359,43 @@ test_go_module_security_dependency_floors() { assert_module_at_least "Go module floor keeps x/sys aligned with security module graph" "${output}" "golang.org/x/sys" "v0.47.0" } +# Kubernetes publishes these generated APIs as a coordinated release. A grouped +# update must not leave the native agent compiling against mixed minor versions. +kubernetes_module_cohort_is_aligned() { + awk ' + $1 == "k8s.io/api" || $1 == "k8s.io/apimachinery" || $1 == "k8s.io/client-go" { + if (seen[$1]++) exit 1 + if (version != "" && version != $2) exit 1 + version = $2 + count++ + } + END { if (count != 3 || version == "") exit 1 } + ' +} + +test_go_kubernetes_module_cohort() { + local output + output="$(cd "${ROOT_DIR}" && go list -mod=readonly -m k8s.io/api k8s.io/apimachinery k8s.io/client-go)" + if printf '%s\n' "${output}" | kubernetes_module_cohort_is_aligned; then + echo "[PASS] native Kubernetes module cohort is aligned" + else + echo "[FAIL] native Kubernetes module cohort is incomplete or mismatched" >&2 + ((failures++)) + fi + if printf '%s\n' 'k8s.io/api v0.37.0' 'k8s.io/apimachinery v0.36.2' 'k8s.io/client-go v0.37.0' | kubernetes_module_cohort_is_aligned; then + echo "[FAIL] Kubernetes cohort check accepted mixed releases" >&2 + ((failures++)) + else + echo "[PASS] Kubernetes cohort check rejects mixed releases" + fi + if printf '%s\n' 'k8s.io/api v0.37.0' 'k8s.io/client-go v0.37.0' | kubernetes_module_cohort_is_aligned; then + echo "[FAIL] Kubernetes cohort check accepted a missing module" >&2 + ((failures++)) + else + echo "[PASS] Kubernetes cohort check rejects a missing module" + fi +} + test_go_release_toolchain_floor() { local module_toolchain installed_toolchain module_toolchain="$(sed -n 's/^toolchain //p' "${ROOT_DIR}/go.mod")" @@ -390,6 +427,7 @@ test_hot_dev_lab_agent_mode_enables_lan_and_guest_docker_inventory_defaults test_hot_dev_remembers_explicit_lab_agent_mode_for_later_managed_starts test_hot_dev_browser_urls_distinguish_bind_and_browser_hosts test_go_module_security_dependency_floors +test_go_kubernetes_module_cohort test_go_release_toolchain_floor test_backend_race_suite_keeps_hosted_runner_timeout_headroom From eccb64d155aeda3c146e8a222a1e4a6fc5cd6468 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:00:21 +0100 Subject: [PATCH 4/5] fix(alerts): honour PBS datastore capacity policy Keep topology capacity bands as resource risk rather than duplicate parent and datastore alerts. The existing live PBS capacity evaluator owns thresholds, aliases and hysteresis; retain datastore state failures and reconcile old duplicates normally. Reproduce the 97.9% report with real registry projection and exercise threshold changes through synthetic PBS HTTP polling. Release adaptations require the live-poll evaluator, not this filter alone. Change-source: pulse-maintainer --- .../v6/internal/subsystems/alerts.md | 24 ++++ internal/alerts/unified_incidents.go | 13 ++ internal/alerts/unified_incidents_test.go | 127 +++++++++++++++--- .../monitoring/monitor_pbs_coverage_test.go | 25 +++- 4 files changed, 169 insertions(+), 20 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 49dc1123c..fd7a30e86 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -2856,3 +2856,27 @@ This changes neither delivery scheduling nor the meaning of a successful test. The registered WebhookConfig regression covers alias test/save payload equality; the browser fixture exercises the real form at desktop and phone widths with synthetic callbacks, not a hosted Pushover destination or installed delivery. + +### PBS capacity has one alert policy owner + +PBS datastore capacity alerts are evaluated by `CheckStorageWithCapacityTrend` +from fresh PBS polls. Storage defaults, canonical datastore aliases, per-resource +overrides, hysteresis and predictive capacity policy govern that lifecycle. +The fixed 90/95% PBS topology assessment remains resource risk evidence; its +Pulse-generated `capacity_runway_low` incidents must not independently enter +active alerts on either the datastore or the parent backup server. Existing +copies retire through normal policy reconciliation, without deleting state or +claiming that capacity itself recovered. Other datastore state/error incidents +and native provider incidents retain their existing lifecycle. + +`TestPBSCapacityUsesStoragePolicyNotTopologyBands` in +`internal/alerts/unified_incidents_test.go` uses real registry projection to pin +both duplicate symptoms at 97.9%, canonical-alias 99% versus 90% policy, existing +alert retirement, threshold recovery, and preservation of datastore failure. +The posture and roll-up tests use datastore state failures, independent of +capacity. This relies on the live PBS poll evaluator; a release adaptation must +include that evaluator rather than remove the topology alerts in isolation. +`TestPBSPolledCapacityRequiresObservedRecovery` in +`internal/monitoring/monitor_pbs_coverage_test.go` additionally exercises the +97.9% policy transition through synthetic PBS HTTP polling, storage conversion +and unified alert synchronisation, including absence of duplicate parent alerts. diff --git a/internal/alerts/unified_incidents.go b/internal/alerts/unified_incidents.go index 4130235da..cb1239e78 100644 --- a/internal/alerts/unified_incidents.go +++ b/internal/alerts/unified_incidents.go @@ -94,6 +94,19 @@ func (m *Manager) SyncUnifiedResourceIncidents(resources []unifiedresources.Reso storageKey := canonicalTrackingKeyForSpec(spec, alert.ID) observedConditions[storageKey] = struct{}{} + // PBS capacity is evaluated by CheckStorageWithCapacityTrend using + // storage defaults, aliases, overrides and hysteresis. The topology's + // fixed risk bands are resource context, not a second alert policy. + // Keep the condition observed so pre-upgrade duplicates retire as a + // policy change without claiming that datastore health recovered. + if strings.EqualFold(strings.TrimSpace(incident.Provider), "pulse") && + incident.Code == "capacity_runway_low" && + (resource.Type == unifiedresources.ResourceTypePBS || + (resource.Type == unifiedresources.ResourceTypeStorage && resource.Storage != nil && + resource.Storage.Platform == "pbs" && resource.Storage.Type == "pbs-datastore")) { + continue + } + if alertType, ok := unifiedAlertResourceType(resource); ok { if disableAllKubernetes && isUnifiedKubernetesAlertType(alertType) { continue diff --git a/internal/alerts/unified_incidents_test.go b/internal/alerts/unified_incidents_test.go index 8bcac0c40..37976b396 100644 --- a/internal/alerts/unified_incidents_test.go +++ b/internal/alerts/unified_incidents_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + alertspecs "github.com/rcourtman/pulse-go-rewrite/internal/alerts/specs" + "github.com/rcourtman/pulse-go-rewrite/internal/models" "github.com/rcourtman/pulse-go-rewrite/internal/operationaltrust" "github.com/rcourtman/pulse-go-rewrite/internal/storagehealth" "github.com/rcourtman/pulse-go-rewrite/internal/truenas" @@ -576,7 +578,7 @@ func TestSyncUnifiedResourceIncidentsMarksPBSBackupPosture(t *testing.T) { Hostname: "pbs-main.local", DatastoreCount: 2, Datastores: []unifiedresources.PBSDatastoreMeta{ - {Name: "fast", Status: "online", Total: 100, Used: 96}, + {Name: "fast", Status: "ERROR", Total: 100, Used: 96}, {Name: "archive", Status: "online", Total: 100, Used: 40}, }, ProtectedWorkloadCount: 2, @@ -585,22 +587,22 @@ func TestSyncUnifiedResourceIncidentsMarksPBSBackupPosture(t *testing.T) { StorageRisk: &unifiedresources.StorageRisk{ Level: storagehealth.RiskCritical, Reasons: []unifiedresources.StorageRiskReason{ - {Code: "capacity_runway_low", Severity: storagehealth.RiskCritical, Summary: "PBS datastore fast is 96% full"}, + {Code: "pbs_datastore_state", Severity: storagehealth.RiskCritical, Summary: "PBS datastore fast is ERROR"}, }, }, }, Incidents: []unifiedresources.ResourceIncident{{ Provider: "pulse", - NativeID: "pbs-instance:pbs-main:capacity_runway_low", - Code: "capacity_runway_low", + NativeID: "pbs-instance:pbs-main:pbs_datastore_state", + Code: "pbs_datastore_state", Severity: storagehealth.RiskCritical, - Summary: "PBS datastore fast is 96% full", + Summary: "PBS datastore fast is ERROR", }}, } m.SyncUnifiedResourceIncidents([]unifiedresources.Resource{resource}) - alertID := "unified-incident-pbs-main-pulse-pbs-instance-pbs-main-capacity-runway-low-capacity-runway-low" + alertID := "unified-incident-pbs-main-pulse-pbs-instance-pbs-main-pbs-datastore-state-pbs-datastore-state" assertAlertPresent(t, m, alertID) m.mu.RLock() @@ -610,7 +612,7 @@ func TestSyncUnifiedResourceIncidentsMarksPBSBackupPosture(t *testing.T) { if alert.Type != "backup-posture-incident" { t.Fatalf("alert type = %q, want backup-posture-incident", alert.Type) } - wantMessage := "Backup server pbs-main has datastore capacity risk. Affects 1 backup datastore: fast" + wantMessage := "Backup server pbs-main has degraded datastore availability. Affects 1 backup datastore: fast" if alert.Message != wantMessage { t.Fatalf("message = %q, want %q", alert.Message, wantMessage) } @@ -734,17 +736,17 @@ func TestSyncUnifiedResourceIncidentsSuppressesPBSDatastoreChildWhenParentRollsU PBS: &unifiedresources.PBSData{ DatastoreCount: 1, Datastores: []unifiedresources.PBSDatastoreMeta{ - {Name: "fast", Status: "online", Total: 100, Used: 96}, + {Name: "fast", Status: "ERROR", Total: 100, Used: 96}, }, ProtectedWorkloadCount: 2, ProtectedWorkloadNames: []string{"media01", "app01"}, }, Incidents: []unifiedresources.ResourceIncident{{ Provider: "pulse", - NativeID: "pbs-instance:pbs-main:capacity_runway_low", - Code: "capacity_runway_low", + NativeID: "pbs-instance:pbs-main:pbs_datastore_state", + Code: "pbs_datastore_state", Severity: storagehealth.RiskCritical, - Summary: "PBS datastore fast is 96% full", + Summary: "PBS datastore fast is ERROR", }}, }, { @@ -761,10 +763,10 @@ func TestSyncUnifiedResourceIncidentsSuppressesPBSDatastoreChildWhenParentRollsU }, Incidents: []unifiedresources.ResourceIncident{{ Provider: "pulse", - NativeID: "pbs-instance:pbs-main:capacity_runway_low", - Code: "capacity_runway_low", + NativeID: "pbs-instance:pbs-main:pbs_datastore_state", + Code: "pbs_datastore_state", Severity: storagehealth.RiskCritical, - Summary: "PBS datastore fast is 96% full", + Summary: "PBS datastore fast is ERROR", }}, }, } @@ -1003,17 +1005,17 @@ func TestGetActiveAlertsPrioritizesBackupPostureExposure(t *testing.T) { PBS: &unifiedresources.PBSData{ DatastoreCount: 1, Datastores: []unifiedresources.PBSDatastoreMeta{ - {Name: "fast", Status: "online", Total: 100, Used: 96}, + {Name: "fast", Status: "ERROR", Total: 100, Used: 96}, }, ProtectedWorkloadCount: 2, ProtectedWorkloadNames: []string{"media01", "app01"}, }, Incidents: []unifiedresources.ResourceIncident{{ Provider: "pulse", - NativeID: "pbs-instance:pbs-main:capacity_runway_low", - Code: "capacity_runway_low", + NativeID: "pbs-instance:pbs-main:pbs_datastore_state", + Code: "pbs_datastore_state", Severity: storagehealth.RiskCritical, - Summary: "PBS datastore fast is 96% full", + Summary: "PBS datastore fast is ERROR", }}, }, { @@ -1332,3 +1334,92 @@ func TestTrueNASNativeCriticalTransition(t *testing.T) { }) } } + +// PBS capacity has one policy owner: CheckStorage. Topology risk remains +// visible, but must not create threshold-independent child/parent alerts. +func TestPBSCapacityUsesStoragePolicyNotTopologyBands(t *testing.T) { + m := newTestManager(t) + config := unifiedEvalBaseConfig() + config.StorageDefault = HysteresisThreshold{Trigger: 90, Clear: 85} + config.Overrides = map[string]ThresholdConfig{"pbs-main/fast": {Usage: &HysteresisThreshold{Trigger: 99, Clear: 98}}} + configureUnifiedEvalManager(t, m, config) + disableTestTimeThresholds(m) + instance := models.PBSInstance{ID: "pbs-main", Name: "main", Status: "online", LastSeen: time.Now(), Datastores: []models.PBSDatastore{{Name: "fast", Status: "online", Total: 1000, Used: 979, Free: 21, Usage: 97.9}}} + registry := unifiedresources.NewRegistry(unifiedresources.NewMemoryStore()) + registry.IngestSnapshot(models.StateSnapshot{PBSInstances: []models.PBSInstance{instance}}) + resources := registry.List() + capacityResources := 0 + for _, r := range resources { + for _, i := range r.Incidents { + if i.Code == "capacity_runway_low" { + capacityResources++ + break + } + } + } + if capacityResources != 2 { + t.Fatalf("want real parent and child capacity evidence, got %d", capacityResources) + } + storage := models.Storage{ID: "pbs-main-fast", AliasIDs: []string{"pbs-main/fast"}, Name: "fast", Instance: "pbs-main", Type: "pbs", Status: "online", Total: 1000, Used: 979, Free: 21, Usage: 97.9} + observe := func() { + for range 5 { + m.CheckStorage(storage) + m.SyncUnifiedResourceIncidents(resources) + } + } + observe() + if active := m.GetActiveAlerts(); len(active) != 0 { + t.Fatalf("99%% policy bypassed by topology incidents: %+v", active) + } + // Seed both pre-upgrade canonical alerts, with unchanged risk evidence. + // The next sync must retire them without deleting resource observations. + m.mu.Lock() + for _, spec := range alertspecs.BuildUnifiedResourceAlertSpecs(resources) { + if spec.Kind != alertspecs.AlertSpecKindProviderIncident { + continue + } + for _, resource := range resources { + if resource.ID != spec.ResourceID { + continue + } + incident, ok := incidentForProviderSpec(resource, spec) + if !ok || incident.Code != "capacity_runway_low" { + continue + } + alert := unifiedIncidentAlert(resource, incident, AlertLevelCritical, time.Now()) + applyCanonicalIdentity(alert, spec.ID, string(spec.Kind)) + m.setActiveAlertNoLock(canonicalTrackingKeyForSpec(spec, alert.ID), alert) + } + } + m.mu.Unlock() + if active := m.GetActiveAlerts(); len(active) != 2 { + t.Fatalf("want two pre-upgrade duplicates, got %d", len(active)) + } + observe() + if active := m.GetActiveAlerts(); len(active) != 0 { + t.Fatalf("pre-upgrade duplicates retained: %+v", active) + } + config.Overrides["pbs-main/fast"] = ThresholdConfig{Usage: &HysteresisThreshold{Trigger: 90, Clear: 85}} + m.UpdateConfig(config) + disableTestTimeThresholds(m) + observe() + active := m.GetActiveAlerts() + if len(active) != 1 || active[0].ResourceID != storage.ID || active[0].CanonicalKind != "metric-threshold" { + t.Fatalf("want one policy-owned capacity alert, got %+v", active) + } + config.Overrides["pbs-main/fast"] = ThresholdConfig{Usage: &HysteresisThreshold{Trigger: 99, Clear: 98}} + m.UpdateConfig(config) + disableTestTimeThresholds(m) + observe() + if active := m.GetActiveAlerts(); len(active) != 0 { + t.Fatalf("raising policy did not clear capacity: %+v", active) + } + // The same nearly-full datastore failing is still actionable. + instance.Datastores[0].Status = "ERROR" + registry.IngestSnapshot(models.StateSnapshot{PBSInstances: []models.PBSInstance{instance}}) + resources = registry.List() + observe() + if active := m.GetActiveAlerts(); len(active) == 0 { + t.Fatal("capacity policy hid datastore failure") + } +} diff --git a/internal/monitoring/monitor_pbs_coverage_test.go b/internal/monitoring/monitor_pbs_coverage_test.go index 34db989b4..a18a4ec6f 100644 --- a/internal/monitoring/monitor_pbs_coverage_test.go +++ b/internal/monitoring/monitor_pbs_coverage_test.go @@ -697,12 +697,33 @@ func TestPBSPolledCapacityRequiresObservedRecovery(t *testing.T) { t.Fatalf("incorrect recovery: %+v", resolved) } // Alternate PBS counter names must feed the same policy and identity. - // Stay below the separate 90% backup-posture incident threshold; the - // configured minimum delta of one permits this immediate recurrence. + // The configured minimum delta of one permits this immediate recurrence. response.Store(`{"data":{"total-space":1000,"used-space":860,"avail-space":140}}`) poll() active = manager.GetActiveAlerts() if len(active) != 1 || active[0].ID != original.ID || active[0].Value != 86 || !active[0].StartTime.After(original.StartTime) { t.Fatalf("incorrect recurrent incident: %+v", active) } + // The reported 97.9% crosses both topology bands. Neither the datastore + // nor parent posture may bypass the UI's 99% capacity policy. + response.Store(`{"data":{"total":1000,"used":979,"avail":21}}`) + highPolicy := basePolicy + highPolicy.Overrides = map[string]alerts.ThresholdConfig{"pbs-pbs-capacity/backups": { + Usage: &alerts.HysteresisThreshold{Trigger: 99, Clear: 98}, + }} + manager.UpdateConfig(highPolicy) + poll() + if active := manager.GetActiveAlerts(); len(active) != 0 { + t.Fatalf("97.9%% poll bypassed 99%% policy with topology incidents: %+v", active) + } + manager.UpdateConfig(basePolicy) + poll() + if active := manager.GetActiveAlerts(); len(active) != 1 || active[0].Type != "usage" { + t.Fatalf("high usage must have one policy-owned alert: %+v", active) + } + manager.UpdateConfig(highPolicy) + poll() + if active := manager.GetActiveAlerts(); len(active) != 0 { + t.Fatalf("raised policy did not clear high-usage alert: %+v", active) + } } From f38d202c63d5f1bef25fba41cc98b021989b7edd Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 20:33:21 +0100 Subject: [PATCH 5/5] style(web): format DOMPurify range regression Apply the repository formatter to the reviewed security-floor cases so the frontend gate can exercise their assertions. Change-source: pulse-maintainer --- .../src/security/__tests__/dependencySecurity.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts index 2cb9a5071..774241c3b 100644 --- a/frontend-modern/src/security/__tests__/dependencySecurity.test.ts +++ b/frontend-modern/src/security/__tests__/dependencySecurity.test.ts @@ -97,9 +97,12 @@ describe('frontend dependency security floors', () => { expect(dompurifyRangeIsPatched(range)).toBe(true); }); - it.each(['^3.4.12', '^3.3.99', '^4.0.0', '^3.4.13-beta.1', '*', '>=3.4.13', '^3.4.13 || ^2.0.0'])('rejects unsafe or unreviewed DOMPurify range %s', (range) => { - expect(dompurifyRangeIsPatched(range)).toBe(false); - }); + it.each(['^3.4.12', '^3.3.99', '^4.0.0', '^3.4.13-beta.1', '*', '>=3.4.13', '^3.4.13 || ^2.0.0'])( + 'rejects unsafe or unreviewed DOMPurify range %s', + (range) => { + expect(dompurifyRangeIsPatched(range)).toBe(false); + }, + ); it('keeps DOMPurify above the hook-detachment XSS floor', () => { expect(dompurifyRangeIsPatched(manifest.dependencies.dompurify)).toBe(true);