@skyhook-io/k8s-ui 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
- package/src/components/gitops/detail-helpers.ts +4 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +412 -5
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
- package/src/components/resources/renderers/PolicySection.tsx +197 -0
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
- package/src/components/resources/resource-utils-cnpg.ts +324 -3
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +12 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +3 -0
- package/src/types/core.ts +29 -0
- package/src/types/gitops-flux-status.test.ts +163 -0
- package/src/types/gitops.ts +80 -24
- package/src/types/index.ts +1 -0
- package/src/types/policy.ts +193 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
package/src/types/core.ts
CHANGED
|
@@ -202,6 +202,11 @@ export type CoreNodeKind =
|
|
|
202
202
|
| 'KubeadmControlPlane' // Cluster API KubeadmControlPlane
|
|
203
203
|
| 'ClusterClass' // Cluster API ClusterClass
|
|
204
204
|
| 'MachineHealthCheck' // Cluster API MachineHealthCheck
|
|
205
|
+
| 'CalicoNetworkPolicy'
|
|
206
|
+
| 'CalicoGlobalNetworkPolicy'
|
|
207
|
+
| 'CalicoStagedNetworkPolicy'
|
|
208
|
+
| 'CalicoStagedGlobalNetworkPolicy'
|
|
209
|
+
| 'CalicoStagedKubernetesNetworkPolicy'
|
|
205
210
|
|
|
206
211
|
// NodeKind can be a core kind or any arbitrary CRD kind string
|
|
207
212
|
export type NodeKind = CoreNodeKind | (string & {})
|
|
@@ -236,6 +241,11 @@ export function displayKind(kind: string): string {
|
|
|
236
241
|
KubeadmControlPlane: 'Control Plane',
|
|
237
242
|
ClusterClass: 'ClusterClass',
|
|
238
243
|
MachineHealthCheck: 'Health Check',
|
|
244
|
+
CalicoNetworkPolicy: 'Calico NetworkPolicy',
|
|
245
|
+
CalicoGlobalNetworkPolicy: 'Calico GlobalNetworkPolicy',
|
|
246
|
+
CalicoStagedNetworkPolicy: 'Calico StagedNetworkPolicy',
|
|
247
|
+
CalicoStagedGlobalNetworkPolicy: 'Calico StagedGlobalNetworkPolicy',
|
|
248
|
+
CalicoStagedKubernetesNetworkPolicy: 'Calico StagedKubernetesNetworkPolicy',
|
|
239
249
|
}
|
|
240
250
|
return shortNames[kind] || kind
|
|
241
251
|
}
|
|
@@ -258,6 +268,7 @@ export interface TopologyEdge {
|
|
|
258
268
|
target: string
|
|
259
269
|
type: EdgeType
|
|
260
270
|
label?: string
|
|
271
|
+
partial?: boolean
|
|
261
272
|
/** Hover tooltip for the edge label. Used by the Reachability view to keep the
|
|
262
273
|
* DECLARED route path available when the label shows an overridden tested path. */
|
|
263
274
|
labelTitle?: string
|
|
@@ -1204,6 +1215,13 @@ export interface TrafficFlow {
|
|
|
1204
1215
|
bytesSent: number
|
|
1205
1216
|
bytesRecv: number
|
|
1206
1217
|
connections: number
|
|
1218
|
+
/** The conversation's initiator could not be established, so source and
|
|
1219
|
+
* destination are ordered arbitrarily rather than describing a caller. */
|
|
1220
|
+
directionUnknown?: boolean
|
|
1221
|
+
/** 5xx responses per second. How a rate-based source reports failures: it
|
|
1222
|
+
* measures a rate rather than observing individual responses, so it has a
|
|
1223
|
+
* status code for no single flow. */
|
|
1224
|
+
errorRate?: number
|
|
1207
1225
|
verdict: string // forwarded, dropped, error
|
|
1208
1226
|
lastSeen: string // ISO date string
|
|
1209
1227
|
}
|
|
@@ -1236,6 +1254,9 @@ export interface AggregatedFlow {
|
|
|
1236
1254
|
bytesRecv: number
|
|
1237
1255
|
connections: number
|
|
1238
1256
|
lastSeen: string
|
|
1257
|
+
/** The conversation's initiator could not be established, so the endpoints are
|
|
1258
|
+
* ordered arbitrarily and the edge is drawn without an arrowhead. */
|
|
1259
|
+
directionUnknown?: boolean
|
|
1239
1260
|
l7Protocol?: string // HTTP, gRPC, DNS
|
|
1240
1261
|
requestCount?: number
|
|
1241
1262
|
errorCount?: number
|
|
@@ -1307,6 +1328,11 @@ export interface TrafficFlowsResponse {
|
|
|
1307
1328
|
flows: TrafficFlow[]
|
|
1308
1329
|
aggregated: AggregatedFlow[]
|
|
1309
1330
|
warning?: string // Non-fatal warning (e.g., query errors)
|
|
1331
|
+
/** 'transient' (or absent) means the condition may clear on its own and a
|
|
1332
|
+
* retry is worthwhile. 'partial' means the flows are correct but incomplete
|
|
1333
|
+
* for a reason retrying cannot change, so show the warning next to them and
|
|
1334
|
+
* do not refetch. */
|
|
1335
|
+
warningKind?: 'transient' | 'partial'
|
|
1310
1336
|
}
|
|
1311
1337
|
|
|
1312
1338
|
// Wizard state for traffic setup
|
|
@@ -1424,6 +1450,9 @@ export interface WorkloadRevision {
|
|
|
1424
1450
|
isCurrent: boolean
|
|
1425
1451
|
replicas: number
|
|
1426
1452
|
template?: string // Pod template spec as YAML (for revision diff)
|
|
1453
|
+
// Rollouts only: mid-canary isCurrent (rolling out) and isStable (serving traffic) differ.
|
|
1454
|
+
isStable?: boolean
|
|
1455
|
+
podHash?: string
|
|
1427
1456
|
}
|
|
1428
1457
|
|
|
1429
1458
|
// Workload log stream event data
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { fluxConditionsToGitOpsStatus, type FluxCondition } from './gitops'
|
|
3
|
+
|
|
4
|
+
const c = (type: string, status: string, extra: Partial<FluxCondition> = {}): FluxCondition =>
|
|
5
|
+
({ type, status, ...extra } as FluxCondition)
|
|
6
|
+
|
|
7
|
+
// Mirrors pkg/gitops/fluxstatus_test.go. The two implementations must not drift:
|
|
8
|
+
// they feed the same columns from different sides of the wire.
|
|
9
|
+
describe('fluxConditionsToGitOpsStatus truth table', () => {
|
|
10
|
+
it('a stale Reconciling does not outvote an applied Ready', () => {
|
|
11
|
+
// The reported bug: kubectl and k9s read the printer columns and show
|
|
12
|
+
// READY=True while Radar showed the row as syncing.
|
|
13
|
+
const got = fluxConditionsToGitOpsStatus(
|
|
14
|
+
[c('Ready', 'True'), c('Healthy', 'True'), c('Reconciling', 'True')],
|
|
15
|
+
false,
|
|
16
|
+
{ generation: 1, observedGeneration: 1 },
|
|
17
|
+
)
|
|
18
|
+
expect(got.sync).toBe('Synced')
|
|
19
|
+
expect(got.health).toBe('Healthy')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('generation drift outranks a stale Ready', () => {
|
|
23
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'True')], false, {
|
|
24
|
+
generation: 2,
|
|
25
|
+
observedGeneration: 1,
|
|
26
|
+
})
|
|
27
|
+
expect(got.sync).toBe('Reconciling')
|
|
28
|
+
expect(got.health).toBe('Progressing')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('treats the -1 sentinel as drift', () => {
|
|
32
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'False'), c('Reconciling', 'True')], false, {
|
|
33
|
+
generation: 1,
|
|
34
|
+
observedGeneration: -1,
|
|
35
|
+
})
|
|
36
|
+
expect(got.sync).toBe('Reconciling')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('does not treat an absent observedGeneration as drift', () => {
|
|
40
|
+
// Kinds and controller versions that never set it must not all read stale.
|
|
41
|
+
expect(fluxConditionsToGitOpsStatus([c('Ready', 'True')], false, { generation: 3 }).sync).toBe('Synced')
|
|
42
|
+
expect(fluxConditionsToGitOpsStatus([c('Ready', 'True')], false, {}).sync).toBe('Synced')
|
|
43
|
+
expect(fluxConditionsToGitOpsStatus([c('Ready', 'True')], false).sync).toBe('Synced')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('lets Stalled outrank Ready', () => {
|
|
47
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'True'), c('Stalled', 'True')], false, {
|
|
48
|
+
generation: 1,
|
|
49
|
+
observedGeneration: 1,
|
|
50
|
+
})
|
|
51
|
+
expect(got.sync).toBe('OutOfSync')
|
|
52
|
+
expect(got.health).toBe('Degraded')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('separates applied from healthy', () => {
|
|
56
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'True'), c('Healthy', 'False')], false, {
|
|
57
|
+
generation: 1,
|
|
58
|
+
observedGeneration: 1,
|
|
59
|
+
})
|
|
60
|
+
expect(got.sync).toBe('Synced')
|
|
61
|
+
expect(got.health).toBe('Degraded')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('reports Reconciling when the pass has not reached Ready', () => {
|
|
65
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'Unknown'), c('Reconciling', 'True')], false, {
|
|
66
|
+
generation: 1,
|
|
67
|
+
observedGeneration: 1,
|
|
68
|
+
})
|
|
69
|
+
expect(got.sync).toBe('Reconciling')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('keeps a suspended-but-applied object Synced, and drops it on drift', () => {
|
|
73
|
+
const gen = { generation: 1, observedGeneration: 1 }
|
|
74
|
+
expect(fluxConditionsToGitOpsStatus([c('Ready', 'True')], true, gen).sync).toBe('Synced')
|
|
75
|
+
expect(
|
|
76
|
+
fluxConditionsToGitOpsStatus([c('Ready', 'True')], true, { generation: 2, observedGeneration: 1 }).sync,
|
|
77
|
+
).toBe('Unknown')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('treats a retrying Ready=False as progressing, not degraded', () => {
|
|
81
|
+
// Mirrors pkg/gitops/fluxstatus_test.go. Flux composes reasons like
|
|
82
|
+
// ProgressingWithRetry; a retry in flight is out of sync but not settled.
|
|
83
|
+
const got = fluxConditionsToGitOpsStatus(
|
|
84
|
+
[c('Ready', 'False', { reason: 'ProgressingWithRetry' })],
|
|
85
|
+
false,
|
|
86
|
+
{ generation: 1, observedGeneration: 1 },
|
|
87
|
+
)
|
|
88
|
+
expect(got.sync).toBe('OutOfSync')
|
|
89
|
+
expect(got.health).toBe('Progressing')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('treats a settled Ready=False as degraded', () => {
|
|
93
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'False', { reason: 'BuildFailed' })], false, {
|
|
94
|
+
generation: 1,
|
|
95
|
+
observedGeneration: 1,
|
|
96
|
+
})
|
|
97
|
+
expect(got.sync).toBe('OutOfSync')
|
|
98
|
+
expect(got.health).toBe('Degraded')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
describe('reconciling activity', () => {
|
|
104
|
+
it('survives even when it no longer drives the status', () => {
|
|
105
|
+
const got = fluxConditionsToGitOpsStatus(
|
|
106
|
+
[c('Ready', 'True'), c('Reconciling', 'True', { lastTransitionTime: '2026-08-11T06:05:00Z' })],
|
|
107
|
+
false,
|
|
108
|
+
{ generation: 1, observedGeneration: 1 },
|
|
109
|
+
)
|
|
110
|
+
expect(got.sync).toBe('Synced')
|
|
111
|
+
expect(got.reconciling).toBe(true)
|
|
112
|
+
expect(got.reconcilingSince).toBe('2026-08-11T06:05:00Z')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('is absent on a quiet object', () => {
|
|
116
|
+
const got = fluxConditionsToGitOpsStatus([c('Ready', 'True')], false, {
|
|
117
|
+
generation: 1,
|
|
118
|
+
observedGeneration: 1,
|
|
119
|
+
})
|
|
120
|
+
expect(got.reconciling).toBeUndefined()
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Source kinds carry no Healthy condition; HelmRelease uses Released.
|
|
125
|
+
describe('non-Kustomization Flux kinds', () => {
|
|
126
|
+
it.each([
|
|
127
|
+
['GitRepository', [c('Ready', 'True'), c('ArtifactInStorage', 'True')]],
|
|
128
|
+
['HelmRepository', [c('Ready', 'True'), c('ArtifactInStorage', 'True')]],
|
|
129
|
+
['HelmRelease', [c('Ready', 'True'), c('Released', 'True')]],
|
|
130
|
+
])('%s reads as Synced/Healthy', (_kind, conds) => {
|
|
131
|
+
const got = fluxConditionsToGitOpsStatus(conds as FluxCondition[], false, {
|
|
132
|
+
generation: 1,
|
|
133
|
+
observedGeneration: 1,
|
|
134
|
+
})
|
|
135
|
+
expect(got.sync).toBe('Synced')
|
|
136
|
+
expect(got.health).toBe('Healthy')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// The wedged-controller case end to end: a fully-applied object whose reconcile
|
|
141
|
+
// pass never finished must read Synced (it IS applied) AND still carry the
|
|
142
|
+
// in-flight fact, or a stuck controller becomes invisible. Before the sync fix
|
|
143
|
+
// this state showed a permanent "Syncing", which was the wrong word but the
|
|
144
|
+
// only thing in the product that noticed.
|
|
145
|
+
describe('a wedged controller stays visible', () => {
|
|
146
|
+
it('reports Synced and keeps the in-flight fact with its start time', () => {
|
|
147
|
+
const got = fluxConditionsToGitOpsStatus(
|
|
148
|
+
[
|
|
149
|
+
c('Ready', 'True', { lastTransitionTime: '2026-08-11T06:00:00Z' }),
|
|
150
|
+
c('Healthy', 'True'),
|
|
151
|
+
c('Reconciling', 'True', { lastTransitionTime: '2026-08-11T06:05:00Z' }),
|
|
152
|
+
],
|
|
153
|
+
false,
|
|
154
|
+
{ generation: 1, observedGeneration: 1 },
|
|
155
|
+
)
|
|
156
|
+
expect(got.sync).toBe('Synced')
|
|
157
|
+
expect(got.health).toBe('Healthy')
|
|
158
|
+
// Both are required: without these the indicator has nothing to render and
|
|
159
|
+
// the operator gets a confident all-clear over a stuck controller.
|
|
160
|
+
expect(got.reconciling).toBe(true)
|
|
161
|
+
expect(got.reconcilingSince).toBe('2026-08-11T06:05:00Z')
|
|
162
|
+
})
|
|
163
|
+
})
|
package/src/types/gitops.ts
CHANGED
|
@@ -63,6 +63,15 @@ export interface GitOpsStatus {
|
|
|
63
63
|
message?: string
|
|
64
64
|
lastSyncTime?: string
|
|
65
65
|
suspended: boolean
|
|
66
|
+
/**
|
|
67
|
+
* A reconcile pass is executing right now. Activity, not sync and not health —
|
|
68
|
+
* it never influences either, because a controller being mid-pass says nothing
|
|
69
|
+
* about whether the declared state is applied. Renderers may show a subtle
|
|
70
|
+
* indicator; nothing that drives a status word.
|
|
71
|
+
*/
|
|
72
|
+
reconciling?: boolean
|
|
73
|
+
/** When the in-flight pass started, so a wedged controller can be told from a routine one. */
|
|
74
|
+
reconcilingSince?: string
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/**
|
|
@@ -97,50 +106,92 @@ export interface FluxCondition {
|
|
|
97
106
|
// ============================================================================
|
|
98
107
|
|
|
99
108
|
/**
|
|
100
|
-
*
|
|
109
|
+
* Generation tracking for a Flux object. `observedGeneration` has three states,
|
|
110
|
+
* and the difference matters: absent means the controller never reports it and
|
|
111
|
+
* we have no signal, which must not be read as drift.
|
|
112
|
+
*/
|
|
113
|
+
export interface FluxGeneration {
|
|
114
|
+
generation?: number
|
|
115
|
+
observedGeneration?: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* True when the controller has demonstrably not yet acted on the current spec.
|
|
120
|
+
* Absent observedGeneration is no signal, not drift — some Flux kinds and older
|
|
121
|
+
* controllers never set it, and treating absence as drift would mark every
|
|
122
|
+
* healthy object on them as out of date. A present-but-different value counts,
|
|
123
|
+
* including the -1 sentinel Flux writes on an object it has never reconciled.
|
|
124
|
+
*/
|
|
125
|
+
function hasGenerationDrift(gen?: FluxGeneration): boolean {
|
|
126
|
+
if (!gen) return false
|
|
127
|
+
const { generation, observedGeneration } = gen
|
|
128
|
+
if (typeof generation !== 'number' || typeof observedGeneration !== 'number') return false
|
|
129
|
+
return generation !== observedGeneration
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Maps FluxCD conditions to unified GitOpsStatus.
|
|
134
|
+
*
|
|
135
|
+
* Sync answers "is the declared state applied", which is `Ready` plus
|
|
136
|
+
* `observedGeneration`. `Reconciling` answers "is the controller mid-pass",
|
|
137
|
+
* which is neither — the two are independent conditions upstream, and letting
|
|
138
|
+
* activity outvote readiness reported a healthy object as not synced.
|
|
101
139
|
*
|
|
102
|
-
*
|
|
103
|
-
* - Ready
|
|
104
|
-
* -
|
|
105
|
-
* - Ready=
|
|
106
|
-
* - Ready
|
|
107
|
-
* -
|
|
108
|
-
* - spec.suspend=true: Suspended
|
|
140
|
+
* Order matters:
|
|
141
|
+
* - suspend → Suspended (sync from Ready, unless drifted)
|
|
142
|
+
* - Stalled=True → OutOfSync / Degraded
|
|
143
|
+
* - Ready=True and no generation drift → Synced, health from Healthy
|
|
144
|
+
* - drift, or Reconciling with Ready not True → Reconciling / Progressing
|
|
145
|
+
* - Ready=False → OutOfSync
|
|
109
146
|
*/
|
|
110
147
|
export function fluxConditionsToGitOpsStatus(
|
|
111
148
|
conditions: FluxCondition[],
|
|
112
|
-
suspended: boolean
|
|
149
|
+
suspended: boolean,
|
|
150
|
+
generation?: FluxGeneration
|
|
113
151
|
): GitOpsStatus {
|
|
114
152
|
const readyCondition = conditions.find(c => c.type === 'Ready')
|
|
115
153
|
const reconcilingCondition = conditions.find(c => c.type === 'Reconciling')
|
|
116
154
|
const stalledCondition = conditions.find(c => c.type === 'Stalled')
|
|
117
155
|
const healthyCondition = conditions.find(c => c.type === 'Healthy')
|
|
118
156
|
|
|
119
|
-
|
|
157
|
+
const drifted = hasGenerationDrift(generation)
|
|
158
|
+
const isReconciling = reconcilingCondition?.status === 'True'
|
|
159
|
+
// Carried on every result so a wedged controller is visible next to an
|
|
160
|
+
// otherwise-correct Synced, without any status word claiming a fault.
|
|
161
|
+
const activity = isReconciling
|
|
162
|
+
? { reconciling: true, reconcilingSince: reconcilingCondition?.lastTransitionTime }
|
|
163
|
+
: {}
|
|
164
|
+
|
|
165
|
+
// Handle suspended state. Suspending does not un-apply what was applied, so a
|
|
166
|
+
// suspended object that was Ready stays Synced — but a spec change made while
|
|
167
|
+
// suspended will never be picked up, so drift downgrades it to Unknown.
|
|
120
168
|
if (suspended) {
|
|
121
169
|
return {
|
|
122
|
-
sync: readyCondition?.status === 'True' ? 'Synced' : 'Unknown',
|
|
170
|
+
sync: readyCondition?.status === 'True' && !drifted ? 'Synced' : 'Unknown',
|
|
123
171
|
health: 'Suspended',
|
|
124
172
|
message: 'Reconciliation is suspended',
|
|
125
173
|
lastSyncTime: readyCondition?.lastTransitionTime,
|
|
126
174
|
suspended: true,
|
|
175
|
+
...activity,
|
|
127
176
|
}
|
|
128
177
|
}
|
|
129
178
|
|
|
130
|
-
//
|
|
131
|
-
|
|
179
|
+
// Stalled is a terminal failure and outranks readiness: it means the
|
|
180
|
+
// controller has given up, whatever the last successful apply left behind.
|
|
181
|
+
if (stalledCondition?.status === 'True') {
|
|
132
182
|
return {
|
|
133
|
-
sync: '
|
|
134
|
-
health: '
|
|
135
|
-
message:
|
|
183
|
+
sync: 'OutOfSync',
|
|
184
|
+
health: 'Degraded',
|
|
185
|
+
message: stalledCondition.message || 'Reconciliation stalled',
|
|
136
186
|
lastSyncTime: readyCondition?.lastTransitionTime,
|
|
137
187
|
suspended: false,
|
|
188
|
+
...activity,
|
|
138
189
|
}
|
|
139
190
|
}
|
|
140
191
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
// The applied state. Ahead of the reconciling branch on purpose — this is the
|
|
193
|
+
// whole point of the ordering.
|
|
194
|
+
if (readyCondition?.status === 'True' && !drifted) {
|
|
144
195
|
const isHealthy = healthyCondition?.status !== 'False'
|
|
145
196
|
return {
|
|
146
197
|
sync: 'Synced',
|
|
@@ -148,17 +199,20 @@ export function fluxConditionsToGitOpsStatus(
|
|
|
148
199
|
message: isHealthy ? undefined : healthyCondition?.message,
|
|
149
200
|
lastSyncTime: readyCondition.lastTransitionTime,
|
|
150
201
|
suspended: false,
|
|
202
|
+
...activity,
|
|
151
203
|
}
|
|
152
204
|
}
|
|
153
205
|
|
|
154
|
-
//
|
|
155
|
-
|
|
206
|
+
// Genuinely not applied: the controller has not observed the current spec, or
|
|
207
|
+
// it is working and has not reached Ready.
|
|
208
|
+
if (drifted || isReconciling) {
|
|
156
209
|
return {
|
|
157
|
-
sync: '
|
|
158
|
-
health: '
|
|
159
|
-
message:
|
|
210
|
+
sync: 'Reconciling',
|
|
211
|
+
health: 'Progressing',
|
|
212
|
+
message: reconcilingCondition?.message || 'Reconciliation in progress',
|
|
160
213
|
lastSyncTime: readyCondition?.lastTransitionTime,
|
|
161
214
|
suspended: false,
|
|
215
|
+
...activity,
|
|
162
216
|
}
|
|
163
217
|
}
|
|
164
218
|
|
|
@@ -173,6 +227,7 @@ export function fluxConditionsToGitOpsStatus(
|
|
|
173
227
|
message: readyCondition.message,
|
|
174
228
|
lastSyncTime: readyCondition.lastTransitionTime,
|
|
175
229
|
suspended: false,
|
|
230
|
+
...activity,
|
|
176
231
|
}
|
|
177
232
|
}
|
|
178
233
|
|
|
@@ -182,6 +237,7 @@ export function fluxConditionsToGitOpsStatus(
|
|
|
182
237
|
health: 'Unknown',
|
|
183
238
|
message: 'Status cannot be determined',
|
|
184
239
|
suspended: false,
|
|
240
|
+
...activity,
|
|
185
241
|
}
|
|
186
242
|
}
|
|
187
243
|
|
package/src/types/index.ts
CHANGED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Per-resource policy findings, from GET /api/policy/resource/{kind}/{ns}/{name}.
|
|
2
|
+
//
|
|
3
|
+
// The coverage fields are not decoration. An empty `findings` array means one of
|
|
4
|
+
// several very different things — nothing violated, no engine installed, the
|
|
5
|
+
// caller may not read the reports, the index is still warming — and a section
|
|
6
|
+
// that renders all of them as blank space tells an operator they are compliant
|
|
7
|
+
// in the cases where we simply do not know.
|
|
8
|
+
|
|
9
|
+
export type PolicyResult = 'pass' | 'fail' | 'warn' | 'error' | 'skip' | string
|
|
10
|
+
|
|
11
|
+
export interface PolicyResourceFinding {
|
|
12
|
+
policy: string
|
|
13
|
+
rule?: string
|
|
14
|
+
result: PolicyResult
|
|
15
|
+
severity?: string
|
|
16
|
+
category?: string
|
|
17
|
+
message?: string
|
|
18
|
+
engine?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PolicyResourceCounts {
|
|
22
|
+
pass: number
|
|
23
|
+
fail: number
|
|
24
|
+
warn: number
|
|
25
|
+
error: number
|
|
26
|
+
skip: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One resource a policy recorded an outcome for, from
|
|
30
|
+
* GET /api/policy/policies/{policy}. */
|
|
31
|
+
export interface PolicyCoverageSubject {
|
|
32
|
+
group?: string
|
|
33
|
+
kind: string
|
|
34
|
+
namespace?: string
|
|
35
|
+
name: string
|
|
36
|
+
result: PolicyResult
|
|
37
|
+
message?: string
|
|
38
|
+
/** Matched through a rule Kyverno synthesised from an authored Pod rule,
|
|
39
|
+
* not through a rule the user wrote. */
|
|
40
|
+
autogen?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PolicyCoverageRule {
|
|
44
|
+
/** Empty for the modern CEL families, which have no named rules. */
|
|
45
|
+
rule: string
|
|
46
|
+
counts: PolicyResourceCounts
|
|
47
|
+
subjects: PolicyCoverageSubject[]
|
|
48
|
+
total: number
|
|
49
|
+
truncated?: boolean
|
|
50
|
+
/** How many of `total` the namespace view filter held back. `counts` still
|
|
51
|
+
* describe every outcome, so the two must be read together. */
|
|
52
|
+
hiddenByFilter?: number
|
|
53
|
+
/** The non-passing part of `hiddenByFilter`. Only non-passing subjects are
|
|
54
|
+
* listed, so this is what separates a row the server cap dropped from one
|
|
55
|
+
* the view filter held back. */
|
|
56
|
+
hiddenNotable?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What one policy decided about the cluster — the inverse of
|
|
60
|
+
* PolicyResourceResponse.
|
|
61
|
+
*
|
|
62
|
+
* Carries one partiality the per-resource view never needed: a resource may be
|
|
63
|
+
* missing because the caller cannot read its namespace, so an absent subject is
|
|
64
|
+
* not evidence of absence. `withheldNamespaces` is how the UI says so. */
|
|
65
|
+
export interface PolicyCoverageResponse {
|
|
66
|
+
evaluated: boolean
|
|
67
|
+
status: string
|
|
68
|
+
reasonCode?: string
|
|
69
|
+
deniedGroups?: string[]
|
|
70
|
+
liveUpdates: boolean
|
|
71
|
+
/** The report-side name that matched — namespaced policies report as
|
|
72
|
+
* "namespace/name". */
|
|
73
|
+
policy: string
|
|
74
|
+
counts: PolicyResourceCounts
|
|
75
|
+
scopeNamespaces: number
|
|
76
|
+
withheldNamespaces: number
|
|
77
|
+
clusterScoped?: boolean
|
|
78
|
+
/** Separate from withheldNamespaces — a cluster-scoped subject has no
|
|
79
|
+
* namespace to name. */
|
|
80
|
+
withheldClusterScoped?: boolean
|
|
81
|
+
/** Outcomes dropped because they came only from a report family this caller
|
|
82
|
+
* cannot read. The server drops them before counting, so the note can state
|
|
83
|
+
* how many were left out rather than hedging that some might have been. */
|
|
84
|
+
withheldByFamily?: number
|
|
85
|
+
/** Report families the CALLER cannot read. Distinct from deniedGroups, which
|
|
86
|
+
* are the ones radar's own probe could not read. */
|
|
87
|
+
unreadableFamilies?: string[]
|
|
88
|
+
/** How many distinct resources these outcomes describe, and how many of those
|
|
89
|
+
* have at least one non-passing outcome. One resource matched by two rules
|
|
90
|
+
* produces two outcomes, so any sentence saying "resources" needs these —
|
|
91
|
+
* `counts` and `examined` are checks. Not derivable client-side: the per-rule
|
|
92
|
+
* subject lists are capped. */
|
|
93
|
+
subjects?: number
|
|
94
|
+
subjectsFailing?: number
|
|
95
|
+
/** How many outcomes back these counts. */
|
|
96
|
+
examined: number
|
|
97
|
+
/** How many subjects the namespace view filter held back, across all rules. */
|
|
98
|
+
hiddenByFilter?: number
|
|
99
|
+
engines?: string[]
|
|
100
|
+
rules: PolicyCoverageRule[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface PolicyResourceResponse {
|
|
104
|
+
/** False whenever the answer is "we could not check". Never treat an empty
|
|
105
|
+
* findings array as "clean" without consulting this first. */
|
|
106
|
+
evaluated: boolean
|
|
107
|
+
status: string
|
|
108
|
+
reasonCode?: string
|
|
109
|
+
/** Report families RADAR's own probe could not read, so nobody's answer
|
|
110
|
+
* carries them — not the same as `withheldByFamily`, which is what THIS
|
|
111
|
+
* caller may not see. Present even when evaluated: the answer is real but
|
|
112
|
+
* incomplete. */
|
|
113
|
+
deniedGroups?: string[]
|
|
114
|
+
/** False when the index is frozen at its initial contents. */
|
|
115
|
+
liveUpdates: boolean
|
|
116
|
+
counts: PolicyResourceCounts
|
|
117
|
+
findings: PolicyResourceFinding[]
|
|
118
|
+
/** Findings dropped because they came only from a report family this caller
|
|
119
|
+
* cannot read. `counts` excludes them too, so an all-passing count with this
|
|
120
|
+
* set is not an all-clear — it is the part of the answer they may see. */
|
|
121
|
+
withheldByFamily?: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Background work Kyverno currently has in flight for one policy, from
|
|
125
|
+
* GET /api/policy/policies/{policy}/queued. Computed server-side: the requests
|
|
126
|
+
* live in the engine's namespace, not the policy's. */
|
|
127
|
+
export interface PolicyQueuedResponse {
|
|
128
|
+
/** Zero is a real answer — Kyverno deletes these seconds after they complete. */
|
|
129
|
+
requests: number
|
|
130
|
+
byState?: Record<string, number>
|
|
131
|
+
/** Separates healthy churn from a stopped controller. */
|
|
132
|
+
oldestPending?: string
|
|
133
|
+
/** The only diagnosis these objects carry. */
|
|
134
|
+
messages?: string[]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One Cluster pinned to a CloudNativePG image catalog. */
|
|
138
|
+
export interface CNPGCatalogUser {
|
|
139
|
+
namespace: string
|
|
140
|
+
name: string
|
|
141
|
+
/** Absent when the reference carries no major — the screen must not render
|
|
142
|
+
* "asks for PostgreSQL 0". */
|
|
143
|
+
major?: number
|
|
144
|
+
/** A catalog-pinned Cluster has no spec.imageName, so this is the only place
|
|
145
|
+
* the image it actually resolved appears. */
|
|
146
|
+
image?: string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Clusters referencing one image catalog, from the CNPG catalog-users endpoint. */
|
|
150
|
+
export interface CNPGCatalogUsersResponse {
|
|
151
|
+
clusters: CNPGCatalogUser[]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** What one Velero BackupStorageLocation holds, from the stored-backups lookup. */
|
|
155
|
+
export interface VeleroStoredBackupsResponse {
|
|
156
|
+
backups: Array<{
|
|
157
|
+
namespace: string
|
|
158
|
+
name: string
|
|
159
|
+
phase?: string
|
|
160
|
+
expiration?: string
|
|
161
|
+
completed?: string
|
|
162
|
+
}>
|
|
163
|
+
/** How many reached Completed and are still inside their retention — the rest
|
|
164
|
+
* are stored here but are not something to restore from. Counted over the
|
|
165
|
+
* whole location, like `stored`: recomputing it from the capped list above
|
|
166
|
+
* undercounts every location big enough to be capped. */
|
|
167
|
+
restorable: number
|
|
168
|
+
/** How many reached Completed but have aged out. Same scope as `restorable`. */
|
|
169
|
+
expired?: number
|
|
170
|
+
/** How many the location holds in total, counted before the list was capped. */
|
|
171
|
+
stored?: number
|
|
172
|
+
/** The list above is a page, not the whole location. */
|
|
173
|
+
truncated?: boolean
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** One warning or error behind a Velero run's counts, with the scope Velero
|
|
177
|
+
* filed it under: "velero", "cluster", or a namespace name. */
|
|
178
|
+
export interface VeleroMessage {
|
|
179
|
+
scope: string
|
|
180
|
+
message: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** What a Backup or Restore actually reported, fetched on demand.
|
|
184
|
+
*
|
|
185
|
+
* The counts live on the CR; these messages do not — they are in a results
|
|
186
|
+
* file in object storage that only Velero's controller can hand out a link to.
|
|
187
|
+
* So this is never present until someone asks for it. */
|
|
188
|
+
export interface VeleroRunMessagesResponse {
|
|
189
|
+
warnings: VeleroMessage[]
|
|
190
|
+
errors: VeleroMessage[]
|
|
191
|
+
/** Set when the results file held more than were returned. */
|
|
192
|
+
truncated?: boolean
|
|
193
|
+
}
|
|
@@ -24,6 +24,11 @@ describe('kindToPlural', () => {
|
|
|
24
24
|
|
|
25
25
|
test('handles kinds ending in consonant+y (changes to -ies)', () => {
|
|
26
26
|
expect(kindToPlural('NetworkPolicy')).toBe('networkpolicies')
|
|
27
|
+
expect(kindToPlural('CalicoNetworkPolicy')).toBe('networkpolicies')
|
|
28
|
+
expect(kindToPlural('CalicoGlobalNetworkPolicy')).toBe('globalnetworkpolicies')
|
|
29
|
+
expect(kindToPlural('CalicoStagedNetworkPolicy')).toBe('stagednetworkpolicies')
|
|
30
|
+
expect(kindToPlural('CalicoStagedGlobalNetworkPolicy')).toBe('stagedglobalnetworkpolicies')
|
|
31
|
+
expect(kindToPlural('CalicoStagedKubernetesNetworkPolicy')).toBe('stagedkubernetesnetworkpolicies')
|
|
27
32
|
})
|
|
28
33
|
|
|
29
34
|
test('handles already-plural kind names (Endpoints)', () => {
|
package/src/utils/navigation.ts
CHANGED
|
@@ -100,6 +100,11 @@ export function kindToPlural(kind: string): string {
|
|
|
100
100
|
horizontalpodautoscaler: 'horizontalpodautoscalers',
|
|
101
101
|
pvc: 'persistentvolumeclaims',
|
|
102
102
|
podgroup: 'pods',
|
|
103
|
+
caliconetworkpolicy: 'networkpolicies',
|
|
104
|
+
calicoglobalnetworkpolicy: 'globalnetworkpolicies',
|
|
105
|
+
calicostagednetworkpolicy: 'stagednetworkpolicies',
|
|
106
|
+
calicostagedglobalnetworkpolicy: 'stagedglobalnetworkpolicies',
|
|
107
|
+
calicostagedkubernetesnetworkpolicy: 'stagedkubernetesnetworkpolicies',
|
|
103
108
|
}
|
|
104
109
|
if (aliases[kindLower]) return aliases[kindLower]
|
|
105
110
|
|