@skyhook-io/k8s-ui 1.10.5 → 1.12.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 +1 -1
- 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/IssuesView.tsx +9 -0
- package/src/components/issues/diagnostic.ts +3 -0
- package/src/components/issues/issues.test.ts +11 -0
- package/src/components/issues/severity.ts +3 -0
- package/src/components/issues/types.ts +3 -0
- package/src/components/resources/ResourcesSidebar.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +261 -30
- package/src/components/resources/index.ts +2 -0
- package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
- package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +133 -9
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +19 -4
- package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
- package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/EventRenderer.tsx +10 -18
- 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 +317 -0
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
- package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
- package/src/components/resources/renderers/PodRenderer.tsx +13 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +102 -0
- package/src/components/resources/renderers/PolicySection.tsx +170 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
- package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
- package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
- package/src/components/resources/renderers/index.ts +5 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +9 -0
- package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
- package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
- package/src/components/resources/renderers/velero-cells.tsx +128 -9
- package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
- package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +590 -1
- package/src/components/resources/resource-utils-cnpg.ts +433 -56
- package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
- package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
- package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
- package/src/components/resources/resource-utils-velero.test.ts +237 -0
- package/src/components/resources/resource-utils-velero.ts +214 -22
- package/src/components/resources/resource-utils.ts +70 -3
- package/src/components/shared/ResourceActionsBar.tsx +3 -1
- package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
- package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
- package/src/components/topology/K8sResourceNode.tsx +6 -0
- package/src/components/trace/ReachabilityGraph.tsx +20 -6
- package/src/components/trace/ReachabilityView.tsx +253 -52
- package/src/components/trace/problemRows.test.ts +107 -0
- package/src/components/trace/reachGraphModel.test.ts +16 -0
- package/src/components/trace/reachInspector.test.ts +233 -10
- package/src/components/trace/reachInspector.ts +166 -36
- package/src/components/trace/reachMarks.test.ts +32 -1
- package/src/components/trace/reachMarks.ts +44 -0
- package/src/components/trace/reachOrigins.ts +12 -0
- package/src/components/trace/types.ts +25 -0
- package/src/components/ui/Badge.tsx +36 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
- package/src/theme/components.css +26 -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 +42 -0
- package/src/utils/api-resources.ts +2 -0
- package/src/utils/resource-icons.test.ts +33 -0
- package/src/utils/resource-icons.ts +65 -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" />
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { renderToString } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { EventRenderer } from './EventRenderer'
|
|
5
|
+
|
|
6
|
+
describe('EventRenderer', () => {
|
|
7
|
+
it.each([
|
|
8
|
+
['Warning', 'status-degraded'],
|
|
9
|
+
['Normal', 'status-neutral'],
|
|
10
|
+
])('uses the theme-aware status tone for %s events', (type, statusClass) => {
|
|
11
|
+
const html = renderToString(<EventRenderer data={{
|
|
12
|
+
type,
|
|
13
|
+
reason: 'FailedScheduling',
|
|
14
|
+
message: 'No nodes are available',
|
|
15
|
+
}} />)
|
|
16
|
+
|
|
17
|
+
expect(html).toContain(statusClass)
|
|
18
|
+
expect(html).toContain('FailedScheduling')
|
|
19
|
+
expect(html).toContain('No nodes are available')
|
|
20
|
+
expect(html).not.toMatch(/text-(?:amber|blue)-(?:200|300|400)/)
|
|
21
|
+
expect(html).not.toContain('opacity-')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AlertTriangle, Info, Clock, Target, Server, Hash } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
+
import { Badge } from '../../ui/Badge'
|
|
3
4
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
5
|
import { formatAge } from '../resource-utils'
|
|
5
6
|
import type { NavigateToResource } from '../../../utils/navigation'
|
|
@@ -20,35 +21,29 @@ export function EventRenderer({ data, onNavigate }: EventRendererProps) {
|
|
|
20
21
|
const source = data.source || {}
|
|
21
22
|
const firstTimestamp = data.firstTimestamp
|
|
22
23
|
const lastTimestamp = data.lastTimestamp || data.metadata?.creationTimestamp
|
|
24
|
+
const statusClass = isWarning ? 'status-degraded' : 'status-neutral'
|
|
25
|
+
const badgeSeverity = isWarning ? 'warning' : 'info'
|
|
23
26
|
|
|
24
27
|
return (
|
|
25
28
|
<>
|
|
26
29
|
{/* Event Type Banner */}
|
|
27
30
|
<div className={clsx(
|
|
28
31
|
'mb-4 p-4 rounded-lg border',
|
|
29
|
-
|
|
30
|
-
? 'bg-amber-500/10 border-amber-500/30'
|
|
31
|
-
: 'bg-blue-500/10 border-blue-500/30'
|
|
32
|
+
statusClass,
|
|
32
33
|
)}>
|
|
33
34
|
<div className="flex items-start gap-3">
|
|
34
35
|
{isWarning ? (
|
|
35
|
-
<AlertTriangle className="w-5 h-5
|
|
36
|
+
<AlertTriangle className="w-5 h-5 mt-0.5 shrink-0" />
|
|
36
37
|
) : (
|
|
37
|
-
<Info className="w-5 h-5
|
|
38
|
+
<Info className="w-5 h-5 mt-0.5 shrink-0" />
|
|
38
39
|
)}
|
|
39
40
|
<div className="flex-1 min-w-0">
|
|
40
41
|
<div className="flex items-center gap-2 mb-1">
|
|
41
|
-
<
|
|
42
|
-
'badge',
|
|
43
|
-
isWarning ? 'bg-amber-500/20 text-amber-400' : 'bg-blue-500/20 text-blue-400'
|
|
44
|
-
)}>
|
|
42
|
+
<Badge severity={badgeSeverity}>
|
|
45
43
|
{eventType}
|
|
46
|
-
</
|
|
44
|
+
</Badge>
|
|
47
45
|
{reason && (
|
|
48
|
-
<span className=
|
|
49
|
-
'text-sm font-semibold',
|
|
50
|
-
isWarning ? 'text-amber-300' : 'text-blue-300'
|
|
51
|
-
)}>
|
|
46
|
+
<span className="text-sm font-semibold">
|
|
52
47
|
{reason}
|
|
53
48
|
</span>
|
|
54
49
|
)}
|
|
@@ -59,10 +54,7 @@ export function EventRenderer({ data, onNavigate }: EventRendererProps) {
|
|
|
59
54
|
)}
|
|
60
55
|
</div>
|
|
61
56
|
{message && (
|
|
62
|
-
<p className=
|
|
63
|
-
'text-sm break-all',
|
|
64
|
-
isWarning ? 'text-amber-200/90' : 'text-blue-200/90'
|
|
65
|
-
)}>
|
|
57
|
+
<p className="text-sm break-all">
|
|
66
58
|
{message}
|
|
67
59
|
</p>
|
|
68
60
|
)}
|
|
@@ -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)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Renderers for Kyverno's modern CEL policy family (policies.kyverno.io),
|
|
2
|
+
// stabilized in Kyverno 1.17/1.18 and the family that survives the removal of
|
|
3
|
+
// kyverno.io Policy/ClusterPolicy planned for 1.20.
|
|
4
|
+
//
|
|
5
|
+
// One renderer per behavioural family, each sharing the posture header and
|
|
6
|
+
// match-scope sections from KyvernoPolicyShared. The five specs overlap far
|
|
7
|
+
// less than their names suggest — DeletingPolicy has no failurePolicy,
|
|
8
|
+
// evaluation or webhookConfiguration at all and is scheduled rather than
|
|
9
|
+
// admission-driven — so a merged renderer would be mostly branches.
|
|
10
|
+
//
|
|
11
|
+
// Each Namespaced* twin renders through its cluster-scoped counterpart: same
|
|
12
|
+
// spec, narrower scope, and the scope is already conveyed by the namespace in
|
|
13
|
+
// the resource header.
|
|
14
|
+
|
|
15
|
+
import { FileWarning, ShieldCheck, Wand2, Sparkles, Trash2, Clock, Fingerprint, ScrollText } from 'lucide-react'
|
|
16
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
17
|
+
import {
|
|
18
|
+
KyvernoEvaluationSection,
|
|
19
|
+
KyvernoExpressionList,
|
|
20
|
+
KyvernoMatchScopeSection,
|
|
21
|
+
KyvernoPostureHeader,
|
|
22
|
+
KyvernoVariablesSection,
|
|
23
|
+
} from './KyvernoPolicyShared'
|
|
24
|
+
import {
|
|
25
|
+
getKyvernoAttestations,
|
|
26
|
+
getKyvernoAttestors,
|
|
27
|
+
getKyvernoDeletionConditions,
|
|
28
|
+
getKyvernoDeletionPropagationPolicy,
|
|
29
|
+
getKyvernoGenerationSettings,
|
|
30
|
+
getKyvernoGenerations,
|
|
31
|
+
getKyvernoImageExtractors,
|
|
32
|
+
getKyvernoImageReferences,
|
|
33
|
+
getKyvernoMutations,
|
|
34
|
+
getKyvernoPolicyConditions,
|
|
35
|
+
getKyvernoReinvocationPolicy,
|
|
36
|
+
getKyvernoSchedule,
|
|
37
|
+
getKyvernoValidations,
|
|
38
|
+
type KyvernoCELValidation,
|
|
39
|
+
} from '../resource-utils-kyverno-modern'
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The CEL rules themselves, each with the message an operator sees when it
|
|
43
|
+
* fails. Expression and message belong together — a rule without its message
|
|
44
|
+
* is unreadable, and a message without its rule is unverifiable.
|
|
45
|
+
*/
|
|
46
|
+
function ValidationsSection({ validations }: { validations: KyvernoCELValidation[] }) {
|
|
47
|
+
if (validations.length === 0) return null
|
|
48
|
+
return (
|
|
49
|
+
<Section title={`Validations (${validations.length})`} icon={FileWarning}>
|
|
50
|
+
<div className="space-y-2">
|
|
51
|
+
{validations.map((v, i) => (
|
|
52
|
+
<div key={i} className="card-inner-lg">
|
|
53
|
+
<pre className="text-xs text-theme-text-primary font-mono whitespace-pre-wrap break-all">
|
|
54
|
+
{v.expression}
|
|
55
|
+
</pre>
|
|
56
|
+
{(v.message || v.messageExpression) && (
|
|
57
|
+
<div className="mt-2 text-xs text-theme-text-secondary">
|
|
58
|
+
{v.message || <span className="font-mono">{v.messageExpression}</span>}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
{v.reason && (
|
|
62
|
+
<div className="mt-1">
|
|
63
|
+
<span className="badge status-neutral">{v.reason}</span>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</Section>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function KyvernoValidatingPolicyRenderer({ data }: { data: any }) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="space-y-4">
|
|
76
|
+
<KyvernoPostureHeader data={data} family="validating" />
|
|
77
|
+
<ValidationsSection validations={getKyvernoValidations(data)} />
|
|
78
|
+
<KyvernoMatchScopeSection data={data} />
|
|
79
|
+
<KyvernoVariablesSection data={data} />
|
|
80
|
+
<KyvernoEvaluationSection data={data} />
|
|
81
|
+
<ConditionsSection conditions={getKyvernoPolicyConditions(data)} />
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
|
|
87
|
+
const attestors = getKyvernoAttestors(data)
|
|
88
|
+
const attestations = getKyvernoAttestations(data)
|
|
89
|
+
const imageRefs = getKyvernoImageReferences(data)
|
|
90
|
+
const extractors = getKyvernoImageExtractors(data)
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="space-y-4">
|
|
94
|
+
<KyvernoPostureHeader data={data} family="imageValidating" />
|
|
95
|
+
|
|
96
|
+
{imageRefs.length > 0 && (
|
|
97
|
+
<Section title={`Image References (${imageRefs.length})`} icon={ScrollText}>
|
|
98
|
+
<div className="space-y-1">
|
|
99
|
+
{imageRefs.map((ref, i) => (
|
|
100
|
+
<div key={i} className="text-xs font-mono text-theme-text-primary break-all">
|
|
101
|
+
{ref}
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</Section>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Attestors are the trust anchor — who must have signed. Without one,
|
|
109
|
+
image verification has nothing to check against. */}
|
|
110
|
+
{attestors.length > 0 ? (
|
|
111
|
+
<Section title={`Attestors (${attestors.length})`} icon={Fingerprint}>
|
|
112
|
+
<div className="space-y-2">
|
|
113
|
+
{attestors.map((a, i) => (
|
|
114
|
+
<div key={i} className="card-inner">
|
|
115
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
116
|
+
{a.name && <span className="text-xs font-medium text-theme-text-primary">{a.name}</span>}
|
|
117
|
+
{a.kind && <span className="badge status-neutral">{a.kind}</span>}
|
|
118
|
+
</div>
|
|
119
|
+
{a.identity && (
|
|
120
|
+
<div className="mt-1 text-xs font-mono text-theme-text-secondary break-all">{a.identity}</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
</Section>
|
|
126
|
+
) : (
|
|
127
|
+
<AlertBanner
|
|
128
|
+
variant="warning"
|
|
129
|
+
title="No attestors configured"
|
|
130
|
+
message="This image policy has no trusted authority to verify signatures against."
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{attestations.length > 0 && (
|
|
135
|
+
<Section title={`Attestations (${attestations.length})`} icon={ShieldCheck}>
|
|
136
|
+
<div className="space-y-2">
|
|
137
|
+
{attestations.map((a, i) => (
|
|
138
|
+
<div key={i} className="card-inner">
|
|
139
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
140
|
+
{a.name && <span className="text-xs font-medium text-theme-text-primary">{a.name}</span>}
|
|
141
|
+
{a.kind && <span className="badge status-neutral">{a.kind}</span>}
|
|
142
|
+
</div>
|
|
143
|
+
{a.type && <div className="mt-1 text-xs font-mono text-theme-text-secondary break-all">{a.type}</div>}
|
|
144
|
+
</div>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
</Section>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
<ValidationsSection validations={getKyvernoValidations(data)} />
|
|
151
|
+
|
|
152
|
+
{extractors.length > 0 && (
|
|
153
|
+
<Section title={`Image Extractors (${extractors.length})`} icon={ScrollText} defaultExpanded={false}>
|
|
154
|
+
<KyvernoExpressionList items={extractors} />
|
|
155
|
+
</Section>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
<KyvernoMatchScopeSection data={data} />
|
|
159
|
+
<KyvernoVariablesSection data={data} />
|
|
160
|
+
<KyvernoEvaluationSection data={data} />
|
|
161
|
+
<ConditionsSection conditions={getKyvernoPolicyConditions(data)} />
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function KyvernoMutatingPolicyRenderer({ data }: { data: any }) {
|
|
167
|
+
const mutations = getKyvernoMutations(data)
|
|
168
|
+
const reinvocation = getKyvernoReinvocationPolicy(data)
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className="space-y-4">
|
|
172
|
+
<KyvernoPostureHeader data={data} family="mutating" />
|
|
173
|
+
|
|
174
|
+
{mutations.length > 0 && (
|
|
175
|
+
<Section title={`Mutations (${mutations.length})`} icon={Wand2}>
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
{mutations.map((m, i) => (
|
|
178
|
+
<div key={i} className="card-inner-lg">
|
|
179
|
+
{m.patchType && (
|
|
180
|
+
<div className="mb-1">
|
|
181
|
+
<span className="badge status-neutral">{m.patchType}</span>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
<pre className="text-xs text-theme-text-primary font-mono whitespace-pre-wrap break-all">
|
|
185
|
+
{m.expression}
|
|
186
|
+
</pre>
|
|
187
|
+
</div>
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
</Section>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{reinvocation && (
|
|
194
|
+
<Section title="Reinvocation" icon={Wand2} defaultExpanded={false}>
|
|
195
|
+
<PropertyList>
|
|
196
|
+
<Property
|
|
197
|
+
label="Policy"
|
|
198
|
+
value={
|
|
199
|
+
<span className="flex items-center gap-2">
|
|
200
|
+
<span className="badge status-neutral">{reinvocation}</span>
|
|
201
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
202
|
+
{reinvocation === 'IfNeeded'
|
|
203
|
+
? 're-runs when another webhook changes the object'
|
|
204
|
+
: 'runs once per admission request'}
|
|
205
|
+
</span>
|
|
206
|
+
</span>
|
|
207
|
+
}
|
|
208
|
+
/>
|
|
209
|
+
</PropertyList>
|
|
210
|
+
</Section>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
<KyvernoMatchScopeSection data={data} />
|
|
214
|
+
<KyvernoVariablesSection data={data} />
|
|
215
|
+
<KyvernoEvaluationSection data={data} />
|
|
216
|
+
<ConditionsSection conditions={getKyvernoPolicyConditions(data)} />
|
|
217
|
+
</div>
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function KyvernoGeneratingPolicyRenderer({ data }: { data: any }) {
|
|
222
|
+
const generations = getKyvernoGenerations(data)
|
|
223
|
+
const settings = getKyvernoGenerationSettings(data)
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className="space-y-4">
|
|
227
|
+
<KyvernoPostureHeader data={data} family="generating" />
|
|
228
|
+
|
|
229
|
+
{generations.length > 0 && (
|
|
230
|
+
<Section title={`Generates (${generations.length})`} icon={Sparkles}>
|
|
231
|
+
<div className="space-y-2">
|
|
232
|
+
{generations.map((expr, i) => (
|
|
233
|
+
<div key={i} className="card-inner-lg">
|
|
234
|
+
<pre className="text-xs text-theme-text-primary font-mono whitespace-pre-wrap break-all">{expr}</pre>
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
</Section>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
{/* These three decide what happens to already-generated objects, which
|
|
242
|
+
is where generation surprises operators: whether existing resources
|
|
243
|
+
are backfilled, whether edits are reverted, and whether downstream
|
|
244
|
+
objects survive deleting the policy. */}
|
|
245
|
+
<Section title="Generation Behavior" icon={Sparkles}>
|
|
246
|
+
<PropertyList>
|
|
247
|
+
<Property
|
|
248
|
+
label="Generate Existing"
|
|
249
|
+
value={settings.generateExisting ? 'Yes — applies to resources that already exist' : 'No — new resources only'}
|
|
250
|
+
/>
|
|
251
|
+
<Property
|
|
252
|
+
label="Synchronize"
|
|
253
|
+
value={
|
|
254
|
+
settings.synchronize
|
|
255
|
+
? 'Yes — manual edits to generated resources are reverted'
|
|
256
|
+
: 'No — generated resources are left alone after creation'
|
|
257
|
+
}
|
|
258
|
+
/>
|
|
259
|
+
<Property
|
|
260
|
+
label="Orphan On Delete"
|
|
261
|
+
value={
|
|
262
|
+
settings.orphanDownstreamOnPolicyDelete
|
|
263
|
+
? 'Yes — generated resources survive deleting this policy'
|
|
264
|
+
: 'No — generated resources are removed with this policy'
|
|
265
|
+
}
|
|
266
|
+
/>
|
|
267
|
+
</PropertyList>
|
|
268
|
+
</Section>
|
|
269
|
+
|
|
270
|
+
<KyvernoMatchScopeSection data={data} />
|
|
271
|
+
<KyvernoVariablesSection data={data} />
|
|
272
|
+
<ConditionsSection conditions={getKyvernoPolicyConditions(data)} />
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function KyvernoDeletingPolicyRenderer({ data }: { data: any }) {
|
|
278
|
+
const schedule = getKyvernoSchedule(data)
|
|
279
|
+
const conditions = getKyvernoDeletionConditions(data)
|
|
280
|
+
const propagation = getKyvernoDeletionPropagationPolicy(data)
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div className="space-y-4">
|
|
284
|
+
<KyvernoPostureHeader data={data} family="deleting" />
|
|
285
|
+
|
|
286
|
+
<Section title="Schedule" icon={Clock}>
|
|
287
|
+
<PropertyList>
|
|
288
|
+
<Property
|
|
289
|
+
label="Cron"
|
|
290
|
+
value={schedule ? <span className="font-mono">{schedule}</span> : 'Not set — this policy will not run'}
|
|
291
|
+
/>
|
|
292
|
+
{propagation && <Property label="Deletion Propagation" value={propagation} />}
|
|
293
|
+
</PropertyList>
|
|
294
|
+
</Section>
|
|
295
|
+
|
|
296
|
+
{/* Without conditions every matched resource is deleted on schedule.
|
|
297
|
+
That is a materially different blast radius and has to be stated
|
|
298
|
+
rather than left as an empty section. */}
|
|
299
|
+
{conditions.length > 0 ? (
|
|
300
|
+
<Section title={`Deletion Conditions (${conditions.length})`} icon={Trash2}>
|
|
301
|
+
<KyvernoExpressionList items={conditions} />
|
|
302
|
+
</Section>
|
|
303
|
+
) : (
|
|
304
|
+
<AlertBanner
|
|
305
|
+
variant="warning"
|
|
306
|
+
icon={Trash2}
|
|
307
|
+
title="No deletion conditions"
|
|
308
|
+
message="Every resource matched by this policy is deleted when the schedule fires."
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<KyvernoMatchScopeSection data={data} />
|
|
313
|
+
<KyvernoVariablesSection data={data} />
|
|
314
|
+
<ConditionsSection conditions={getKyvernoPolicyConditions(data)} />
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
}
|