@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.
Files changed (123) hide show
  1. package/package.json +4 -4
  2. package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
  3. package/src/components/gitops/GitOpsTableView.tsx +22 -2
  4. package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
  5. package/src/components/gitops/detail-helpers.ts +4 -1
  6. package/src/components/issues/severity.ts +1 -0
  7. package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
  8. package/src/components/resources/ResourcesView.tsx +412 -5
  9. package/src/components/resources/index.ts +27 -0
  10. package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
  11. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  12. package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
  13. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
  14. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
  15. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
  16. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
  17. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
  18. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
  19. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
  20. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
  21. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
  22. package/src/components/resources/renderers/CNPGShared.tsx +27 -0
  23. package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
  24. package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
  25. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
  26. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
  27. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
  28. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
  29. package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
  30. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  31. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  32. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  33. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  34. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  35. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  36. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
  37. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
  38. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
  39. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
  40. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
  41. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
  42. package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
  43. package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
  44. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
  45. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
  46. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  47. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  48. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
  49. package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
  50. package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
  51. package/src/components/resources/renderers/PolicySection.tsx +197 -0
  52. package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
  53. package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
  54. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
  55. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
  56. package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
  57. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
  58. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
  59. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
  60. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
  61. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
  62. package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
  63. package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
  64. package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
  65. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
  66. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  67. package/src/components/resources/renderers/calico-cells.tsx +177 -0
  68. package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
  69. package/src/components/resources/renderers/index.ts +16 -0
  70. package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
  71. package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
  72. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  73. package/src/components/resources/renderers/velero-cells.tsx +29 -7
  74. package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
  75. package/src/components/resources/resource-utils-calico.test.ts +215 -0
  76. package/src/components/resources/resource-utils-calico.ts +261 -0
  77. package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
  78. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
  79. package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
  80. package/src/components/resources/resource-utils-cnpg.ts +324 -3
  81. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  82. package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
  83. package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
  84. package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
  85. package/src/components/resources/resource-utils-kyverno.ts +43 -7
  86. package/src/components/resources/resource-utils-velero.test.ts +26 -0
  87. package/src/components/resources/resource-utils-velero.ts +68 -0
  88. package/src/components/resources/resource-utils.ts +98 -0
  89. package/src/components/shared/ResourceActionsBar.tsx +101 -16
  90. package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
  91. package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
  92. package/src/components/shared/index.ts +1 -1
  93. package/src/components/shared/revision-role-badges.test.ts +120 -0
  94. package/src/components/topology/K8sResourceNode.test.ts +10 -0
  95. package/src/components/topology/K8sResourceNode.tsx +22 -0
  96. package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
  97. package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
  98. package/src/components/topology/TopologyGraph.tsx +6 -6
  99. package/src/components/topology/TopologySearch.tsx +7 -0
  100. package/src/components/topology/layout.ts +3 -0
  101. package/src/components/trace/ReachabilityView.tsx +1 -0
  102. package/src/components/trace/TracePanel.tsx +12 -2
  103. package/src/components/trace/reachFixtures.ts +26 -1
  104. package/src/components/trace/reachGraphModel.test.ts +20 -0
  105. package/src/components/trace/reachGraphModel.ts +8 -1
  106. package/src/components/trace/reachOrigins.test.ts +31 -0
  107. package/src/components/trace/reachOrigins.ts +11 -1
  108. package/src/components/ui/Badge.tsx +10 -0
  109. package/src/components/ui/ScheduleValue.test.tsx +80 -0
  110. package/src/components/ui/ScheduleValue.tsx +85 -0
  111. package/src/components/ui/drawer-components.test.tsx +17 -1
  112. package/src/components/ui/drawer-components.tsx +9 -2
  113. package/src/components/ui/index.ts +2 -0
  114. package/src/components/workload/WorkloadView.tsx +3 -0
  115. package/src/types/core.ts +29 -0
  116. package/src/types/gitops-flux-status.test.ts +163 -0
  117. package/src/types/gitops.ts +80 -24
  118. package/src/types/index.ts +1 -0
  119. package/src/types/policy.ts +193 -0
  120. package/src/utils/navigation.test.ts +5 -0
  121. package/src/utils/navigation.ts +5 -0
  122. package/src/utils/resource-icons.test.ts +49 -2
  123. 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
+ })
@@ -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
- * Maps FluxCD conditions to unified GitOpsStatus
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
- * FluxCD status patterns:
103
- * - Ready=True: Synced and healthy
104
- * - Ready=False + Stalled=True: Degraded
105
- * - Ready=False + Reconciling=True: Reconciling
106
- * - Ready=False (other): OutOfSync
107
- * - Healthy=False: Deployed but unhealthy resources
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
- // Handle suspended state
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
- // Handle reconciling state
131
- if (reconcilingCondition?.status === 'True') {
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: 'Reconciling',
134
- health: 'Progressing',
135
- message: reconcilingCondition.message || 'Reconciliation in progress',
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
- // Handle ready state
142
- if (readyCondition?.status === 'True') {
143
- // Check if deployed resources are healthy
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
- // Handle stalled state (permanent failure)
155
- if (stalledCondition?.status === 'True') {
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: 'OutOfSync',
158
- health: 'Degraded',
159
- message: stalledCondition.message || 'Reconciliation stalled',
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
 
@@ -5,3 +5,4 @@ export * from './gitops-tree'
5
5
  export * from './gitops-insights'
6
6
  export * from './rbac'
7
7
  export * from './capacity'
8
+ export * from './policy'
@@ -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)', () => {
@@ -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