@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.
Files changed (80) hide show
  1. package/package.json +1 -1
  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/IssuesView.tsx +9 -0
  7. package/src/components/issues/diagnostic.ts +3 -0
  8. package/src/components/issues/issues.test.ts +11 -0
  9. package/src/components/issues/severity.ts +3 -0
  10. package/src/components/issues/types.ts +3 -0
  11. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  12. package/src/components/resources/ResourcesView.tsx +261 -30
  13. package/src/components/resources/index.ts +2 -0
  14. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  15. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  16. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  17. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  18. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +133 -9
  19. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +19 -4
  20. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  21. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  22. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  23. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  24. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  25. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  26. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  27. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  28. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  29. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  30. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  31. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  32. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  33. package/src/components/resources/renderers/PolicySection.test.tsx +102 -0
  34. package/src/components/resources/renderers/PolicySection.tsx +170 -0
  35. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  36. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  37. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  38. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  39. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  40. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  41. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  42. package/src/components/resources/renderers/index.ts +5 -0
  43. package/src/components/resources/renderers/kyverno-cells.tsx +9 -0
  44. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  45. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  46. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  47. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  48. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  49. package/src/components/resources/resource-utils-cnpg.test.ts +590 -1
  50. package/src/components/resources/resource-utils-cnpg.ts +433 -56
  51. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  52. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  53. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  54. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  55. package/src/components/resources/resource-utils-velero.ts +214 -22
  56. package/src/components/resources/resource-utils.ts +70 -3
  57. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  58. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  59. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  60. package/src/components/topology/K8sResourceNode.tsx +6 -0
  61. package/src/components/trace/ReachabilityGraph.tsx +20 -6
  62. package/src/components/trace/ReachabilityView.tsx +253 -52
  63. package/src/components/trace/problemRows.test.ts +107 -0
  64. package/src/components/trace/reachGraphModel.test.ts +16 -0
  65. package/src/components/trace/reachInspector.test.ts +233 -10
  66. package/src/components/trace/reachInspector.ts +166 -36
  67. package/src/components/trace/reachMarks.test.ts +32 -1
  68. package/src/components/trace/reachMarks.ts +44 -0
  69. package/src/components/trace/reachOrigins.ts +12 -0
  70. package/src/components/trace/types.ts +25 -0
  71. package/src/components/ui/Badge.tsx +36 -0
  72. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  73. package/src/theme/components.css +26 -0
  74. package/src/types/gitops-flux-status.test.ts +163 -0
  75. package/src/types/gitops.ts +80 -24
  76. package/src/types/index.ts +1 -0
  77. package/src/types/policy.ts +42 -0
  78. package/src/utils/api-resources.ts +2 -0
  79. package/src/utils/resource-icons.test.ts +33 -0
  80. 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
- <div className="text-sm text-theme-text-tertiary py-2">
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
- <span className="text-theme-text-tertiary font-mono shrink-0 w-32 truncate">{ref_.kind}</span>
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-32 text-right">{ref_.namespace}</span>
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
- isWarning
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 text-amber-400 mt-0.5 shrink-0" />
36
+ <AlertTriangle className="w-5 h-5 mt-0.5 shrink-0" />
36
37
  ) : (
37
- <Info className="w-5 h-5 text-blue-400 mt-0.5 shrink-0" />
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
- <span className={clsx(
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
- </span>
44
+ </Badge>
47
45
  {reason && (
48
- <span className={clsx(
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={clsx(
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
+ }