@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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { CompositeRenderer } from './CompositeRenderer'
|
|
4
|
+
import type { CrossplaneResourceRef } from '../resource-utils-crossplane'
|
|
5
|
+
|
|
6
|
+
const claim = {
|
|
7
|
+
apiVersion: 'platform.example.org/v1alpha1',
|
|
8
|
+
kind: 'DatabaseClaim',
|
|
9
|
+
metadata: { name: 'example-database', namespace: 'demo-app' },
|
|
10
|
+
spec: {
|
|
11
|
+
compositionRef: { name: 'database' },
|
|
12
|
+
resourceRef: {
|
|
13
|
+
apiVersion: 'platform.example.org/v1alpha1',
|
|
14
|
+
kind: 'Database',
|
|
15
|
+
name: 'example-database-x7k2m',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const composed: CrossplaneResourceRef[] = [
|
|
21
|
+
{ apiVersion: 'database.example.org/v1beta1', kind: 'Instance', name: 'example-database-instance' },
|
|
22
|
+
{ apiVersion: 'identity.example.org/v1beta1', kind: 'Role', name: 'example-database-access' },
|
|
23
|
+
{ apiVersion: 'kubernetes.crossplane.io/v1alpha2', kind: 'Object', name: 'example-database-connection' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const html = (props: any) => renderToString(<CompositeRenderer {...props} />)
|
|
27
|
+
|
|
28
|
+
describe('CompositeRenderer — claim composed-resources panel (issue coverage)', () => {
|
|
29
|
+
it('lists composed resources resolved from the bound XR (the fix)', () => {
|
|
30
|
+
const out = html({ data: claim, composedRefs: composed })
|
|
31
|
+
expect(out).toContain('Composed Resources (3)')
|
|
32
|
+
for (const ref of composed) {
|
|
33
|
+
expect(out).toContain(ref.name)
|
|
34
|
+
}
|
|
35
|
+
expect(out).not.toContain('No composed resources yet')
|
|
36
|
+
// claim identity section + the bound-XR link
|
|
37
|
+
expect(out).toContain('Claim')
|
|
38
|
+
expect(out).toContain('Bound Composite')
|
|
39
|
+
expect(out).toContain('example-database-x7k2m')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('shows a loading state while the bound XR is being fetched — not "none"', () => {
|
|
43
|
+
const out = html({ data: claim, composedRefs: [], boundXRStatus: { loading: true } })
|
|
44
|
+
expect(out).toContain('Loading composed resources')
|
|
45
|
+
expect(out).not.toContain('No composed resources yet')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('distinguishes a not-found / unreadable bound XR from genuinely-none', () => {
|
|
49
|
+
const out = html({ data: claim, composedRefs: [], boundXRStatus: { missing: true } })
|
|
50
|
+
expect(out).toContain('was not found')
|
|
51
|
+
expect(out).not.toContain('No composed resources yet')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('surfaces a bound-XR read error (RBAC/500) distinctly, with the message', () => {
|
|
55
|
+
const out = html({
|
|
56
|
+
data: claim,
|
|
57
|
+
composedRefs: [],
|
|
58
|
+
boundXRStatus: { error: true, errorMessage: 'forbidden: cannot get databases' },
|
|
59
|
+
})
|
|
60
|
+
expect(out).toContain('read the bound composite') // apostrophe is HTML-escaped in SSR output
|
|
61
|
+
expect(out).toContain('forbidden: cannot get databases')
|
|
62
|
+
expect(out).not.toContain('No composed resources yet')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('falls back to the genuine empty state for a claim whose XR simply has none yet', () => {
|
|
66
|
+
const out = html({ data: claim, composedRefs: [] })
|
|
67
|
+
expect(out).toContain('No composed resources yet')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('for an XR viewed directly (no bound-XR status), reads its own resourceRefs', () => {
|
|
71
|
+
const xr = {
|
|
72
|
+
apiVersion: 'platform.example.org/v1alpha1',
|
|
73
|
+
kind: 'Database',
|
|
74
|
+
metadata: { name: 'example-database-x7k2m' },
|
|
75
|
+
spec: { resourceRefs: composed },
|
|
76
|
+
}
|
|
77
|
+
const out = html({ data: xr })
|
|
78
|
+
expect(out).toContain('Composed Resources (3)')
|
|
79
|
+
expect(out).toContain('Composite Resource') // not "Claim"
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -52,6 +52,28 @@ interface CompositeRendererProps {
|
|
|
52
52
|
* of an unrelated Bucket from another provider. Populated by the host
|
|
53
53
|
* wrapper. When absent, the renderer shows refs without status badges. */
|
|
54
54
|
composedRefStatuses?: Map<string, ComposedRefStatus>
|
|
55
|
+
/** Composed-resource refs resolved by the host. A v1 Claim carries only a
|
|
56
|
+
* singular spec.resourceRef to its bound XR — the composed refs live on that
|
|
57
|
+
* XR, not the claim — so the host follows the ref, fetches the XR, and passes
|
|
58
|
+
* its refs here. When omitted, refs are read off `data` directly (correct for
|
|
59
|
+
* an XR/Composite viewed on its own). */
|
|
60
|
+
composedRefs?: CrossplaneResourceRef[]
|
|
61
|
+
/** State of the host's fetch of the claim's bound XR. Set only for a v1 Claim.
|
|
62
|
+
* Without it, a claim whose XR is still loading or unreadable (404/RBAC) would
|
|
63
|
+
* fall into the empty branch and read "No composed resources yet" — which is
|
|
64
|
+
* indistinguishable from a claim that genuinely has none. */
|
|
65
|
+
boundXRStatus?: BoundXRStatus
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Fetch state of a claim's bound XR, reported by the host wrapper. */
|
|
69
|
+
export interface BoundXRStatus {
|
|
70
|
+
/** true while the XR fetch is in flight */
|
|
71
|
+
loading?: boolean
|
|
72
|
+
/** 404 — the referenced XR does not exist (dangling resourceRef) */
|
|
73
|
+
missing?: boolean
|
|
74
|
+
/** any non-404 failure (auth, network, server error); message in errorMessage */
|
|
75
|
+
error?: boolean
|
|
76
|
+
errorMessage?: string
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
function makeRefKey(ref: CrossplaneResourceRef): string {
|
|
@@ -67,13 +89,13 @@ function groupFromApiVersion(apiVersion: string | undefined): string {
|
|
|
67
89
|
return slash < 0 ? '' : apiVersion.slice(0, slash)
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
export function CompositeRenderer({ data, onNavigate, composedRefStatuses }: CompositeRendererProps) {
|
|
92
|
+
export function CompositeRenderer({ data, onNavigate, composedRefStatuses, composedRefs, boundXRStatus }: CompositeRendererProps) {
|
|
71
93
|
const status = getCrossplaneStatus(data)
|
|
72
94
|
const reason = getCrossplaneStatusReason(data)
|
|
73
95
|
const compositionRef = getCompositionRef(data)
|
|
74
96
|
const compositionRevisionRef = getCompositionRevisionRef(data)
|
|
75
97
|
const compositionUpdatePolicy = getCompositionUpdatePolicy(data)
|
|
76
|
-
const resourceRefs = getCrossplaneResourceRefs(data)
|
|
98
|
+
const resourceRefs = composedRefs ?? getCrossplaneResourceRefs(data)
|
|
77
99
|
const claim = isClaim(data)
|
|
78
100
|
const boundXR = claim ? getBoundXRRef(data) : null
|
|
79
101
|
const paused = isCrossplanePaused(data)
|
|
@@ -153,9 +175,7 @@ export function CompositeRenderer({ data, onNavigate, composedRefStatuses }: Com
|
|
|
153
175
|
defaultExpanded
|
|
154
176
|
>
|
|
155
177
|
{resourceRefs.length === 0 ? (
|
|
156
|
-
<
|
|
157
|
-
No composed resources yet — the composition has not produced any managed resources.
|
|
158
|
-
</div>
|
|
178
|
+
<ComposedResourcesEmpty boundXRStatus={boundXRStatus} boundXRName={boundXR?.name} />
|
|
159
179
|
) : (
|
|
160
180
|
<div className="space-y-1">
|
|
161
181
|
{resourceRefs.map((ref) => (
|
|
@@ -175,6 +195,44 @@ export function CompositeRenderer({ data, onNavigate, composedRefStatuses }: Com
|
|
|
175
195
|
)
|
|
176
196
|
}
|
|
177
197
|
|
|
198
|
+
function ComposedResourcesEmpty({
|
|
199
|
+
boundXRStatus,
|
|
200
|
+
boundXRName,
|
|
201
|
+
}: {
|
|
202
|
+
boundXRStatus?: BoundXRStatus
|
|
203
|
+
boundXRName?: string
|
|
204
|
+
}) {
|
|
205
|
+
if (boundXRStatus?.loading) {
|
|
206
|
+
return (
|
|
207
|
+
<div className="text-sm text-theme-text-tertiary py-2">
|
|
208
|
+
Loading composed resources from the bound composite…
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
if (boundXRStatus?.missing) {
|
|
213
|
+
const named = boundXRName ? ` (${boundXRName})` : ''
|
|
214
|
+
return (
|
|
215
|
+
<div className="text-sm text-theme-text-tertiary py-2">
|
|
216
|
+
The bound composite{named} referenced by this claim was not found — it may not have been
|
|
217
|
+
created yet, or you may not have permission to read it.
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
if (boundXRStatus?.error) {
|
|
222
|
+
const named = boundXRName ? ` (${boundXRName})` : ''
|
|
223
|
+
return (
|
|
224
|
+
<div className="text-sm status-unhealthy rounded px-2 py-2">
|
|
225
|
+
Couldn't read the bound composite{named}: {boundXRStatus.errorMessage || 'fetch failed'}
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return (
|
|
230
|
+
<div className="text-sm text-theme-text-tertiary py-2">
|
|
231
|
+
No composed resources yet — the composition has not produced any managed resources.
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
178
236
|
function ComposedRefRow({
|
|
179
237
|
ref_,
|
|
180
238
|
statusEntry,
|
|
@@ -189,7 +247,9 @@ function ComposedRefRow({
|
|
|
189
247
|
return (
|
|
190
248
|
<div className="flex items-center gap-2 py-1 px-2 rounded hover:bg-theme-hover text-sm">
|
|
191
249
|
<Box className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
|
|
192
|
-
|
|
250
|
+
{/* Kind is content-sized (capped) so the resource name — the field that
|
|
251
|
+
distinguishes rows — keeps the width instead of truncating. */}
|
|
252
|
+
<span className="text-theme-text-tertiary font-mono shrink-0 max-w-[8rem] truncate">{ref_.kind}</span>
|
|
193
253
|
<span className="flex-1 min-w-0 truncate">
|
|
194
254
|
<ResourceLink
|
|
195
255
|
name={ref_.name}
|
|
@@ -200,7 +260,7 @@ function ComposedRefRow({
|
|
|
200
260
|
/>
|
|
201
261
|
</span>
|
|
202
262
|
{ref_.namespace && (
|
|
203
|
-
<span className="text-xs text-theme-text-tertiary truncate w-
|
|
263
|
+
<span className="text-xs text-theme-text-tertiary truncate max-w-[8rem] shrink-0 text-right">{ref_.namespace}</span>
|
|
204
264
|
)}
|
|
205
265
|
<StatusCellInline statusEntry={statusEntry} />
|
|
206
266
|
<ChevronRight className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
|
|
@@ -17,7 +17,10 @@ export function FluxHelmReleaseRenderer({ data, onNavigate }: FluxHelmReleaseRen
|
|
|
17
17
|
const history = status.history || []
|
|
18
18
|
|
|
19
19
|
// Convert to unified GitOps status
|
|
20
|
-
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true
|
|
20
|
+
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
|
|
21
|
+
generation: data.metadata?.generation,
|
|
22
|
+
observedGeneration: status.observedGeneration,
|
|
23
|
+
})
|
|
21
24
|
|
|
22
25
|
// Problem detection
|
|
23
26
|
const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
|
|
@@ -16,7 +16,10 @@ export function GitRepositoryRenderer({ data }: GitRepositoryRendererProps) {
|
|
|
16
16
|
const artifact = status.artifact || {}
|
|
17
17
|
|
|
18
18
|
// Convert to unified GitOps status
|
|
19
|
-
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true
|
|
19
|
+
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
|
|
20
|
+
generation: data.metadata?.generation,
|
|
21
|
+
observedGeneration: status.observedGeneration,
|
|
22
|
+
})
|
|
20
23
|
|
|
21
24
|
// Problem detection
|
|
22
25
|
const problems: Array<{ color: 'red' | 'yellow'; message: string }> = []
|
|
@@ -17,7 +17,10 @@ export function HelmRepositoryRenderer({ data }: HelmRepositoryRendererProps) {
|
|
|
17
17
|
const artifact = status.artifact || {}
|
|
18
18
|
|
|
19
19
|
// Convert to unified GitOps status
|
|
20
|
-
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true
|
|
20
|
+
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
|
|
21
|
+
generation: data.metadata?.generation,
|
|
22
|
+
observedGeneration: status.observedGeneration,
|
|
23
|
+
})
|
|
21
24
|
|
|
22
25
|
// Determine repository type
|
|
23
26
|
const isOCI = spec.type === 'oci'
|
|
@@ -16,7 +16,10 @@ export function KustomizationRenderer({ data, onNavigate }: KustomizationRendere
|
|
|
16
16
|
const inventoryEntries = status.inventory?.entries || []
|
|
17
17
|
|
|
18
18
|
// Convert to unified GitOps status
|
|
19
|
-
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true
|
|
19
|
+
const gitOpsStatus = fluxConditionsToGitOpsStatus(conditions, spec.suspend === true, {
|
|
20
|
+
generation: data.metadata?.generation,
|
|
21
|
+
observedGeneration: status.observedGeneration,
|
|
22
|
+
})
|
|
20
23
|
|
|
21
24
|
// Parse inventory to managed resources
|
|
22
25
|
const managedResources = parseFluxInventory(inventoryEntries)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type React from 'react'
|
|
1
2
|
// Renderers for Kyverno's modern CEL policy family (policies.kyverno.io),
|
|
2
3
|
// stabilized in Kyverno 1.17/1.18 and the family that survives the removal of
|
|
3
4
|
// kyverno.io Policy/ClusterPolicy planned for 1.20.
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
|
|
15
16
|
import { FileWarning, ShieldCheck, Wand2, Sparkles, Trash2, Clock, Fingerprint, ScrollText } from 'lucide-react'
|
|
16
17
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
18
|
+
import { CronValue } from '../../ui/ScheduleValue'
|
|
17
19
|
import {
|
|
18
20
|
KyvernoEvaluationSection,
|
|
19
21
|
KyvernoExpressionList,
|
|
@@ -70,10 +72,11 @@ function ValidationsSection({ validations }: { validations: KyvernoCELValidation
|
|
|
70
72
|
)
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export function KyvernoValidatingPolicyRenderer({ data }: { data: any }) {
|
|
75
|
+
export function KyvernoValidatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
74
76
|
return (
|
|
75
77
|
<div className="space-y-4">
|
|
76
78
|
<KyvernoPostureHeader data={data} family="validating" />
|
|
79
|
+
{coverage}
|
|
77
80
|
<ValidationsSection validations={getKyvernoValidations(data)} />
|
|
78
81
|
<KyvernoMatchScopeSection data={data} />
|
|
79
82
|
<KyvernoVariablesSection data={data} />
|
|
@@ -83,7 +86,7 @@ export function KyvernoValidatingPolicyRenderer({ data }: { data: any }) {
|
|
|
83
86
|
)
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
|
|
89
|
+
export function KyvernoImageValidatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
87
90
|
const attestors = getKyvernoAttestors(data)
|
|
88
91
|
const attestations = getKyvernoAttestations(data)
|
|
89
92
|
const imageRefs = getKyvernoImageReferences(data)
|
|
@@ -92,6 +95,7 @@ export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
|
|
|
92
95
|
return (
|
|
93
96
|
<div className="space-y-4">
|
|
94
97
|
<KyvernoPostureHeader data={data} family="imageValidating" />
|
|
98
|
+
{coverage}
|
|
95
99
|
|
|
96
100
|
{imageRefs.length > 0 && (
|
|
97
101
|
<Section title={`Image References (${imageRefs.length})`} icon={ScrollText}>
|
|
@@ -163,13 +167,14 @@ export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
|
|
|
163
167
|
)
|
|
164
168
|
}
|
|
165
169
|
|
|
166
|
-
export function KyvernoMutatingPolicyRenderer({ data }: { data: any }) {
|
|
170
|
+
export function KyvernoMutatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
167
171
|
const mutations = getKyvernoMutations(data)
|
|
168
172
|
const reinvocation = getKyvernoReinvocationPolicy(data)
|
|
169
173
|
|
|
170
174
|
return (
|
|
171
175
|
<div className="space-y-4">
|
|
172
176
|
<KyvernoPostureHeader data={data} family="mutating" />
|
|
177
|
+
{coverage}
|
|
173
178
|
|
|
174
179
|
{mutations.length > 0 && (
|
|
175
180
|
<Section title={`Mutations (${mutations.length})`} icon={Wand2}>
|
|
@@ -218,13 +223,14 @@ export function KyvernoMutatingPolicyRenderer({ data }: { data: any }) {
|
|
|
218
223
|
)
|
|
219
224
|
}
|
|
220
225
|
|
|
221
|
-
export function KyvernoGeneratingPolicyRenderer({ data }: { data: any }) {
|
|
226
|
+
export function KyvernoGeneratingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
222
227
|
const generations = getKyvernoGenerations(data)
|
|
223
228
|
const settings = getKyvernoGenerationSettings(data)
|
|
224
229
|
|
|
225
230
|
return (
|
|
226
231
|
<div className="space-y-4">
|
|
227
232
|
<KyvernoPostureHeader data={data} family="generating" />
|
|
233
|
+
{coverage}
|
|
228
234
|
|
|
229
235
|
{generations.length > 0 && (
|
|
230
236
|
<Section title={`Generates (${generations.length})`} icon={Sparkles}>
|
|
@@ -274,7 +280,7 @@ export function KyvernoGeneratingPolicyRenderer({ data }: { data: any }) {
|
|
|
274
280
|
)
|
|
275
281
|
}
|
|
276
282
|
|
|
277
|
-
export function KyvernoDeletingPolicyRenderer({ data }: { data: any }) {
|
|
283
|
+
export function KyvernoDeletingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
278
284
|
const schedule = getKyvernoSchedule(data)
|
|
279
285
|
const conditions = getKyvernoDeletionConditions(data)
|
|
280
286
|
const propagation = getKyvernoDeletionPropagationPolicy(data)
|
|
@@ -282,12 +288,13 @@ export function KyvernoDeletingPolicyRenderer({ data }: { data: any }) {
|
|
|
282
288
|
return (
|
|
283
289
|
<div className="space-y-4">
|
|
284
290
|
<KyvernoPostureHeader data={data} family="deleting" />
|
|
291
|
+
{coverage}
|
|
285
292
|
|
|
286
293
|
<Section title="Schedule" icon={Clock}>
|
|
287
294
|
<PropertyList>
|
|
288
295
|
<Property
|
|
289
296
|
label="Cron"
|
|
290
|
-
value={schedule ? <
|
|
297
|
+
value={schedule ? <CronValue cron={schedule} /> : 'Not set — this policy will not run'}
|
|
291
298
|
/>
|
|
292
299
|
{propagation && <Property label="Deletion Propagation" value={propagation} />}
|
|
293
300
|
</PropertyList>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
AlertBanner,
|
|
15
15
|
LabelSelectorDisplay,
|
|
16
16
|
} from '../../ui/drawer-components'
|
|
17
|
+
import { CronValue, TimeValue } from '../../ui/ScheduleValue'
|
|
17
18
|
import { KyvernoExpressionList } from './KyvernoPolicyShared'
|
|
18
19
|
import {
|
|
19
20
|
getKyvernoCleanupConditions,
|
|
@@ -225,9 +226,9 @@ export function KyvernoCleanupPolicyRenderer({ data }: { data: any }) {
|
|
|
225
226
|
|
|
226
227
|
<Section title="Schedule" icon={Clock}>
|
|
227
228
|
<PropertyList>
|
|
228
|
-
<Property label="Cron" value={schedule ? <
|
|
229
|
+
<Property label="Cron" value={schedule ? <CronValue cron={schedule} /> : 'Not set'} />
|
|
229
230
|
{propagation && <Property label="Deletion Propagation" value={propagation} />}
|
|
230
|
-
{lastExecution && <Property label="Last Execution" value={lastExecution} />}
|
|
231
|
+
{lastExecution && <Property label="Last Execution" value={<TimeValue timestamp={lastExecution} />} />}
|
|
231
232
|
</PropertyList>
|
|
232
233
|
</Section>
|
|
233
234
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getKyvernoContextRefresh } from './KyvernoGlobalContextRenderer'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kyverno records a successful refresh and records nothing at all for a failed
|
|
6
|
+
* one — no message, no condition. Verified on 1.18.2: `cluster-namespaces` came
|
|
7
|
+
* back with `lastRefreshTime`, `broken-lookup` came back with an empty status.
|
|
8
|
+
* Age is therefore the only thing separating "failing" from "not yet".
|
|
9
|
+
*/
|
|
10
|
+
describe('global context refresh state', () => {
|
|
11
|
+
const ago = (ms: number) => new Date(Date.now() - ms).toISOString()
|
|
12
|
+
|
|
13
|
+
it('is healthy once it has refreshed', () => {
|
|
14
|
+
const r = getKyvernoContextRefresh({ status: { lastRefreshTime: ago(60_000) } })
|
|
15
|
+
expect(r.refreshed).toBe(true)
|
|
16
|
+
expect(r.level).toBe('healthy')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('is unknown while the entry is young enough not to have run yet', () => {
|
|
20
|
+
const r = getKyvernoContextRefresh({ metadata: { creationTimestamp: ago(10_000) }, status: {} })
|
|
21
|
+
expect(r.level).toBe('unknown')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Old and never refreshed is the diagnosis; anything sooner would accuse the
|
|
25
|
+
// entry before its first attempt.
|
|
26
|
+
it('is unhealthy once an old entry still has no refresh', () => {
|
|
27
|
+
const r = getKyvernoContextRefresh({ metadata: { creationTimestamp: ago(10 * 60_000) }, status: {} })
|
|
28
|
+
expect(r.level).toBe('unhealthy')
|
|
29
|
+
expect(r.refreshed).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Globe, RefreshCw } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { HEALTH_BADGE_COLORS } from '../../../utils/badge-colors'
|
|
5
|
+
import { formatAge, healthColors } from '../resource-utils'
|
|
6
|
+
import type { StatusBadge } from '../resource-utils'
|
|
7
|
+
|
|
8
|
+
type Nav = (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether the entry has ever produced data.
|
|
12
|
+
*
|
|
13
|
+
* Kyverno records success as `status.lastRefreshTime` and records failure as
|
|
14
|
+
* nothing at all — there is no message, no condition, no reason. So the only
|
|
15
|
+
* honest reading is "it has refreshed" versus "it has never refreshed", and the
|
|
16
|
+
* age of the entry is what turns the second into a diagnosis. Inventing a cause
|
|
17
|
+
* for a silent failure would be worse than naming the silence.
|
|
18
|
+
*/
|
|
19
|
+
export function getKyvernoContextRefresh(resource: any): {
|
|
20
|
+
refreshed: boolean
|
|
21
|
+
at?: string
|
|
22
|
+
level: 'healthy' | 'unhealthy' | 'unknown'
|
|
23
|
+
} {
|
|
24
|
+
const at = resource?.status?.lastRefreshTime
|
|
25
|
+
if (typeof at === 'string' && at !== '') return { refreshed: true, at, level: 'healthy' }
|
|
26
|
+
// Young enough that the first refresh may simply not have happened yet.
|
|
27
|
+
const created = resource?.metadata?.creationTimestamp
|
|
28
|
+
const ageMs = created ? Date.now() - new Date(created).getTime() : 0
|
|
29
|
+
return { refreshed: false, level: ageMs > 2 * 60 * 1000 ? 'unhealthy' : 'unknown' }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GlobalContextEntry — external data a policy reads while it evaluates.
|
|
34
|
+
*
|
|
35
|
+
* It matters because a policy referencing an entry that never resolves fails at
|
|
36
|
+
* evaluation time, and the failure surfaces as a policy error with no obvious
|
|
37
|
+
* cause. This page is where that cause lives.
|
|
38
|
+
*/
|
|
39
|
+
export function KyvernoGlobalContextRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
|
|
40
|
+
const spec = data?.spec ?? {}
|
|
41
|
+
const api = spec.apiCall
|
|
42
|
+
const cm = spec.configMap
|
|
43
|
+
const refresh = getKyvernoContextRefresh(data)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{refresh.level === 'unhealthy' && (
|
|
48
|
+
<AlertBanner
|
|
49
|
+
variant="error"
|
|
50
|
+
title="This entry has never refreshed"
|
|
51
|
+
// Deliberately not a diagnosis: Kyverno records no reason for a failed
|
|
52
|
+
// refresh, so the page reports the fact and where to look.
|
|
53
|
+
message="Kyverno records no reason when a refresh fails. Any policy referencing this entry will error while it evaluates. Check the source below is reachable and that Kyverno may read it."
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
<Section title="Refresh" icon={RefreshCw} defaultExpanded>
|
|
58
|
+
<PropertyList>
|
|
59
|
+
<Property
|
|
60
|
+
label="State"
|
|
61
|
+
value={
|
|
62
|
+
<span
|
|
63
|
+
className={clsx('badge', HEALTH_BADGE_COLORS[refresh.level as keyof typeof HEALTH_BADGE_COLORS])}
|
|
64
|
+
>
|
|
65
|
+
{refresh.refreshed ? 'Refreshed' : refresh.level === 'unhealthy' ? 'Never refreshed' : 'Not refreshed yet'}
|
|
66
|
+
</span>
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
{refresh.at && <Property label="Last Refreshed" value={`${formatAge(refresh.at)} ago`} />}
|
|
70
|
+
{api?.refreshInterval && <Property label="Interval" value={api.refreshInterval} />}
|
|
71
|
+
{api?.retryLimit != null && <Property label="Retry Limit" value={String(api.retryLimit)} />}
|
|
72
|
+
</PropertyList>
|
|
73
|
+
{!refresh.refreshed && refresh.level === 'unknown' && (
|
|
74
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">
|
|
75
|
+
Created moments ago — the first refresh may not have run yet.
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</Section>
|
|
79
|
+
|
|
80
|
+
<Section title="Source" icon={Globe} defaultExpanded>
|
|
81
|
+
{api ? (
|
|
82
|
+
<PropertyList>
|
|
83
|
+
<Property label="Kind" value="Kubernetes API call" />
|
|
84
|
+
<Property label="Method" value={api.method || 'GET'} />
|
|
85
|
+
<Property label="Path" value={<span className="font-mono text-xs">{api.urlPath || '-'}</span>} />
|
|
86
|
+
{api.jmesPath && (
|
|
87
|
+
<Property label="Filter" value={<span className="font-mono text-xs">{api.jmesPath}</span>} />
|
|
88
|
+
)}
|
|
89
|
+
</PropertyList>
|
|
90
|
+
) : cm ? (
|
|
91
|
+
<PropertyList>
|
|
92
|
+
<Property label="Kind" value="ConfigMap" />
|
|
93
|
+
<Property
|
|
94
|
+
label="ConfigMap"
|
|
95
|
+
value={
|
|
96
|
+
<ResourceLink
|
|
97
|
+
name={cm.name}
|
|
98
|
+
kind="configmaps"
|
|
99
|
+
namespace={cm.namespace ?? ''}
|
|
100
|
+
onNavigate={onNavigate}
|
|
101
|
+
/>
|
|
102
|
+
}
|
|
103
|
+
/>
|
|
104
|
+
{cm.namespace && <Property label="Namespace" value={cm.namespace} />}
|
|
105
|
+
</PropertyList>
|
|
106
|
+
) : (
|
|
107
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
108
|
+
No API call or ConfigMap source is declared, so this entry can never produce data.
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</Section>
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** List badge for GlobalContextEntry, mirroring the detail page's three states. */
|
|
117
|
+
export function getKyvernoGlobalContextStatus(resource: any): StatusBadge {
|
|
118
|
+
const r = getKyvernoContextRefresh(resource)
|
|
119
|
+
if (r.refreshed) return { text: 'Refreshed', color: healthColors.healthy, level: 'healthy' }
|
|
120
|
+
if (r.level === 'unhealthy') {
|
|
121
|
+
return { text: 'Never Refreshed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
122
|
+
}
|
|
123
|
+
return { text: 'Pending', color: healthColors.unknown, level: 'unknown' }
|
|
124
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type React from 'react'
|
|
1
2
|
import { Shield, ShieldCheck, ShieldAlert, FileWarning, ListChecks, ChevronDown, ChevronRight } from 'lucide-react'
|
|
2
3
|
import { clsx } from 'clsx'
|
|
3
4
|
import { useState } from 'react'
|
|
@@ -225,6 +226,13 @@ export function PolicyReportRenderer({ data }: PolicyReportRendererProps) {
|
|
|
225
226
|
|
|
226
227
|
interface KyvernoPolicyRendererProps {
|
|
227
228
|
data: any
|
|
229
|
+
/** Filled by the host with the policy's coverage section — what this policy
|
|
230
|
+
* actually decided about the cluster. */
|
|
231
|
+
coverage?: React.ReactNode
|
|
232
|
+
/** Filled by the host with the work this policy has queued and not finished.
|
|
233
|
+
* The failure worth catching here is a pile-up, not one request, so it
|
|
234
|
+
* belongs beside the policy rather than only on its own page. */
|
|
235
|
+
queued?: React.ReactNode
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
const ruleTypeColorMap: Record<string, string> = {
|
|
@@ -234,7 +242,7 @@ const ruleTypeColorMap: Record<string, string> = {
|
|
|
234
242
|
verifyImages: 'bg-orange-500/20 text-orange-400',
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
245
|
+
export function KyvernoPolicyRenderer({ data, coverage, queued }: KyvernoPolicyRendererProps) {
|
|
238
246
|
const spec = data.spec || {}
|
|
239
247
|
const status = data.status || {}
|
|
240
248
|
const conditions = status.conditions || []
|
|
@@ -249,9 +257,18 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
|
249
257
|
|
|
250
258
|
return (
|
|
251
259
|
<>
|
|
260
|
+
{coverage}
|
|
261
|
+
{queued}
|
|
252
262
|
{/* Configuration */}
|
|
253
263
|
<Section title="Configuration" icon={Shield}>
|
|
254
264
|
<PropertyList>
|
|
265
|
+
{/* Audit / Enforce governs the rules that can REJECT — validation and
|
|
266
|
+
image verification. A policy whose rules only generate or mutate
|
|
267
|
+
carries the field inertly, and showing it as the policy's mode
|
|
268
|
+
reads as "this one only reports" about a policy that is actively
|
|
269
|
+
writing to the cluster. status.rulecount is Kyverno's own count,
|
|
270
|
+
so this is checkable rather than guessed. */}
|
|
271
|
+
{(ruleCountByType.validate > 0 || ruleCountByType.verifyImages > 0) && (
|
|
255
272
|
<Property label="Failure Action" value={
|
|
256
273
|
<span className={clsx(
|
|
257
274
|
'badge',
|
|
@@ -260,6 +277,7 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
|
260
277
|
{action}
|
|
261
278
|
</span>
|
|
262
279
|
} />
|
|
280
|
+
)}
|
|
263
281
|
<Property label="Background" value={background ? 'Enabled' : 'Disabled'} />
|
|
264
282
|
{spec.webhookTimeoutSeconds && (
|
|
265
283
|
<Property label="Webhook Timeout" value={`${spec.webhookTimeoutSeconds}s`} />
|
|
@@ -342,7 +360,15 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
|
342
360
|
{status.rulecount.validate !== undefined && <Property label="Validate" value={status.rulecount.validate} />}
|
|
343
361
|
{status.rulecount.mutate !== undefined && <Property label="Mutate" value={status.rulecount.mutate} />}
|
|
344
362
|
{status.rulecount.generate !== undefined && <Property label="Generate" value={status.rulecount.generate} />}
|
|
345
|
-
{
|
|
363
|
+
{/* All-lowercase in Kyverno's own status, so the camelCase read
|
|
364
|
+
dropped the only non-zero row on an image-verification policy
|
|
365
|
+
and left three zeros reading as "no rules". */}
|
|
366
|
+
{(status.rulecount.verifyImages ?? status.rulecount.verifyimages) !== undefined && (
|
|
367
|
+
<Property
|
|
368
|
+
label="Verify Images"
|
|
369
|
+
value={status.rulecount.verifyImages ?? status.rulecount.verifyimages}
|
|
370
|
+
/>
|
|
371
|
+
)}
|
|
346
372
|
</PropertyList>
|
|
347
373
|
</Section>
|
|
348
374
|
)}
|