mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Merge pull request #2012 from rcourtman/maintainer/20260909T185522Z
Honor PBS capacity thresholds and keep dependency upgrades safe
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user