Merge pull request #2012 from rcourtman/maintainer/20260909T185522Z

Honor PBS capacity thresholds and keep dependency upgrades safe
This commit is contained in:
pulse-triage[bot]
2026-09-09 20:07:14 +00:00
committed by GitHub
24 changed files with 457 additions and 82 deletions
+1 -1
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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'
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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'
@@ -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.
@@ -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.
@@ -5344,3 +5344,32 @@ 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.
### 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.
@@ -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.
@@ -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,23 @@ 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);
}
});
+24 -13
View File
@@ -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
)
+48
View File
@@ -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=
+13
View File
@@ -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
+109 -18
View File
@@ -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")
}
}
@@ -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)
@@ -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)
}
}
@@ -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)
}
}
})
}
}
+3
View File
@@ -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) {
+38
View File
@@ -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
+25
View File
@@ -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: