@skyhook-io/k8s-ui 1.10.4 → 1.11.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 (99) hide show
  1. package/package.json +5 -5
  2. package/src/components/applications/ApplicationDetail.test.tsx +46 -0
  3. package/src/components/applications/ApplicationDetail.tsx +14 -15
  4. package/src/components/issues/IssuesView.tsx +9 -0
  5. package/src/components/issues/diagnostic.ts +3 -0
  6. package/src/components/issues/issues.test.ts +11 -0
  7. package/src/components/issues/severity.ts +3 -0
  8. package/src/components/issues/types.ts +3 -0
  9. package/src/components/resources/ResourcesSidebar.tsx +1 -1
  10. package/src/components/resources/ResourcesView.tsx +259 -30
  11. package/src/components/resources/index.ts +2 -0
  12. package/src/components/resources/kyverno-cell-gating.test.ts +93 -0
  13. package/src/components/resources/kyverno-modern-posture.test.ts +290 -0
  14. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +85 -0
  15. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +109 -9
  16. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +8 -4
  17. package/src/components/resources/renderers/EventRenderer.test.tsx +23 -0
  18. package/src/components/resources/renderers/EventRenderer.tsx +10 -18
  19. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +317 -0
  20. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +264 -0
  21. package/src/components/resources/renderers/KyvernoPolicyShared.tsx +212 -0
  22. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +2 -4
  23. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +32 -9
  24. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +30 -9
  25. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +12 -6
  26. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +1 -1
  27. package/src/components/resources/renderers/cnpg-cells.tsx +37 -2
  28. package/src/components/resources/renderers/index.ts +4 -0
  29. package/src/components/resources/renderers/kyverno-modern-cells.tsx +152 -0
  30. package/src/components/resources/renderers/velero-cells.tsx +128 -9
  31. package/src/components/resources/renderers/velero-phase-recovery.test.tsx +79 -0
  32. package/src/components/resources/resource-utils-cnpg.golden.test.ts +112 -0
  33. package/src/components/resources/resource-utils-cnpg.test.ts +527 -1
  34. package/src/components/resources/resource-utils-cnpg.ts +373 -56
  35. package/src/components/resources/resource-utils-kyverno-exceptions.ts +182 -0
  36. package/src/components/resources/resource-utils-kyverno-modern.ts +588 -0
  37. package/src/components/resources/resource-utils-velero.test.ts +237 -0
  38. package/src/components/resources/resource-utils-velero.ts +214 -22
  39. package/src/components/resources/resource-utils.ts +70 -3
  40. package/src/components/shared/ResourceActionsBar.tsx +3 -1
  41. package/src/components/shared/ResourceRendererDispatch.test.tsx +119 -1
  42. package/src/components/shared/ResourceRendererDispatch.tsx +116 -22
  43. package/src/components/topology/K8sResourceNode.tsx +11 -0
  44. package/src/components/topology/TopologyGraph.tsx +85 -5
  45. package/src/components/trace/ReachabilityGraph.tsx +517 -0
  46. package/src/components/trace/ReachabilityView.tsx +1173 -0
  47. package/src/components/trace/TracePanel.test.ts +172 -0
  48. package/src/components/trace/TracePanel.tsx +558 -0
  49. package/src/components/trace/TraceSummary.test.ts +213 -0
  50. package/src/components/trace/TraceSummary.tsx +168 -0
  51. package/src/components/trace/inClusterSummary.test.ts +98 -0
  52. package/src/components/trace/inClusterSummary.ts +81 -0
  53. package/src/components/trace/index.ts +19 -0
  54. package/src/components/trace/podReach.test.ts +71 -0
  55. package/src/components/trace/podReach.ts +37 -0
  56. package/src/components/trace/probe-display.test.ts +107 -0
  57. package/src/components/trace/probe-display.ts +37 -0
  58. package/src/components/trace/problemRows.test.ts +107 -0
  59. package/src/components/trace/reachFixtures.ts +261 -0
  60. package/src/components/trace/reachGraphModel.test.ts +1423 -0
  61. package/src/components/trace/reachGraphModel.ts +1521 -0
  62. package/src/components/trace/reachInspector.test.ts +892 -0
  63. package/src/components/trace/reachInspector.ts +797 -0
  64. package/src/components/trace/reachMarks.test.ts +698 -0
  65. package/src/components/trace/reachMarks.ts +623 -0
  66. package/src/components/trace/reachOrigins.test.ts +392 -0
  67. package/src/components/trace/reachOrigins.ts +449 -0
  68. package/src/components/trace/reachVerdict.test.ts +537 -0
  69. package/src/components/trace/reachVerdict.ts +601 -0
  70. package/src/components/trace/traceFingerprint.test.ts +214 -0
  71. package/src/components/trace/traceFingerprint.ts +122 -0
  72. package/src/components/trace/traceToSubgraph.test.ts +623 -0
  73. package/src/components/trace/traceToSubgraph.ts +463 -0
  74. package/src/components/trace/types.ts +414 -0
  75. package/src/components/ui/Badge.tsx +36 -0
  76. package/src/components/ui/ForceDeleteConfirmDialog.tsx +12 -1
  77. package/src/components/ui/InClusterConsentDialog.test.tsx +83 -0
  78. package/src/components/ui/InClusterConsentDialog.tsx +141 -0
  79. package/src/components/ui/index.ts +1 -0
  80. package/src/components/workload/WorkloadView.tsx +58 -5
  81. package/src/components/workload/index.ts +1 -1
  82. package/src/components/workload/reachabilityTab.test.ts +47 -0
  83. package/src/index.ts +5 -0
  84. package/src/theme/components.css +64 -0
  85. package/src/theme/variables.css +5 -0
  86. package/src/types/core.ts +8 -0
  87. package/src/utils/api-resources.ts +2 -0
  88. package/src/utils/application-history.test.ts +1 -1
  89. package/src/utils/applications.test.ts +63 -1
  90. package/src/utils/applications.ts +27 -0
  91. package/src/utils/inClusterConsent.test.ts +109 -0
  92. package/src/utils/inClusterConsent.ts +72 -0
  93. package/src/utils/index.ts +1 -0
  94. package/src/utils/navigation.test.ts +61 -0
  95. package/src/utils/navigation.ts +11 -9
  96. package/src/utils/pluralize.test.ts +80 -1
  97. package/src/utils/pluralize.ts +34 -0
  98. package/src/utils/resource-icons.test.ts +33 -0
  99. package/src/utils/resource-icons.ts +65 -2
@@ -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
  )}
@@ -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
+ }
@@ -0,0 +1,264 @@
1
+ // PolicyException and CleanupPolicy renderers.
2
+ //
3
+ // PolicyException is the record of who was let off the hook — the most
4
+ // operationally interesting Kyverno object after the policy itself. One
5
+ // renderer serves both API families; the accessors it calls are
6
+ // group-qualified, so the same component reads whichever shape it is handed.
7
+
8
+ import { ShieldOff, Filter, Clock, Trash2, AlertTriangle, ScrollText } from 'lucide-react'
9
+ import {
10
+ Section,
11
+ PropertyList,
12
+ Property,
13
+ ConditionsSection,
14
+ AlertBanner,
15
+ LabelSelectorDisplay,
16
+ } from '../../ui/drawer-components'
17
+ import { KyvernoExpressionList } from './KyvernoPolicyShared'
18
+ import {
19
+ getKyvernoCleanupConditions,
20
+ getKyvernoCleanupExclude,
21
+ getKyvernoCleanupLastExecution,
22
+ getKyvernoCleanupMatch,
23
+ getKyvernoCleanupMatchedKinds,
24
+ getKyvernoCleanupPropagationPolicy,
25
+ getKyvernoCleanupSchedule,
26
+ getKyvernoExceptedPolicies,
27
+ getKyvernoExceptionLegacyConditions,
28
+ getKyvernoExceptionMatch,
29
+ getKyvernoExceptionMatchConditions,
30
+ getKyvernoExceptionReportResult,
31
+ isBlanketKyvernoException,
32
+ isModernKyvernoPolicyException,
33
+ } from '../resource-utils-kyverno-exceptions'
34
+
35
+ /**
36
+ * Renders the legacy any/all match block. Each entry carries a
37
+ * `resources` selector with kinds/names/namespaces/selector.
38
+ */
39
+ function LegacyMatchBlock({ match }: { match: any }) {
40
+ const blocks: Array<{ operator: string; entries: any[] }> = []
41
+ if (Array.isArray(match?.any)) blocks.push({ operator: 'any', entries: match.any })
42
+ if (Array.isArray(match?.all)) blocks.push({ operator: 'all', entries: match.all })
43
+ if (blocks.length === 0) return null
44
+
45
+ return (
46
+ <div className="space-y-2">
47
+ {blocks.map((block) => (
48
+ <div key={block.operator}>
49
+ <div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">Match {block.operator}</div>
50
+ <div className="space-y-2">
51
+ {block.entries.map((entry, i) => {
52
+ const resources = entry?.resources || {}
53
+ return (
54
+ <div key={i} className="card-inner">
55
+ <PropertyList>
56
+ {Array.isArray(resources.kinds) && resources.kinds.length > 0 && (
57
+ <Property label="Kinds" value={resources.kinds.join(', ')} />
58
+ )}
59
+ {Array.isArray(resources.names) && resources.names.length > 0 && (
60
+ <Property label="Names" value={resources.names.join(', ')} />
61
+ )}
62
+ {Array.isArray(resources.namespaces) && resources.namespaces.length > 0 && (
63
+ <Property label="Namespaces" value={resources.namespaces.join(', ')} />
64
+ )}
65
+ {resources.selector && (
66
+ <Property label="Selector" value={<LabelSelectorDisplay selector={resources.selector} inline />} />
67
+ )}
68
+ {resources.namespaceSelector && (
69
+ <Property
70
+ label="Namespace Selector"
71
+ value={<LabelSelectorDisplay selector={resources.namespaceSelector} inline />}
72
+ />
73
+ )}
74
+ </PropertyList>
75
+ </div>
76
+ )
77
+ })}
78
+ </div>
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )
83
+ }
84
+
85
+ export function KyvernoPolicyExceptionRenderer({ data }: { data: any }) {
86
+ const isModern = isModernKyvernoPolicyException(data)
87
+ const excepted = getKyvernoExceptedPolicies(data)
88
+ const blanket = isBlanketKyvernoException(data)
89
+ const matchConditions = getKyvernoExceptionMatchConditions(data)
90
+ const legacyMatch = getKyvernoExceptionMatch(data)
91
+ const legacyConditions = getKyvernoExceptionLegacyConditions(data)
92
+ const reportResult = getKyvernoExceptionReportResult(data)
93
+
94
+ return (
95
+ <div className="space-y-4">
96
+ {/* An exception naming no policy exempts everything in its match scope.
97
+ That is the widest exemption expressible here and the operator has to
98
+ see it before anything else. */}
99
+ {blanket && (
100
+ <AlertBanner
101
+ variant="warning"
102
+ icon={AlertTriangle}
103
+ title="Blanket exception"
104
+ message="This exception names no specific policy, so every policy matching its scope is bypassed."
105
+ />
106
+ )}
107
+
108
+ <div className="card-inner-lg">
109
+ <div className="flex items-center gap-2 flex-wrap">
110
+ <span className="text-xs uppercase tracking-wide text-theme-text-tertiary">Exempts</span>
111
+ <span className={`badge ${blanket ? 'status-alert' : 'status-neutral'}`}>
112
+ {blanket ? 'All matching policies' : `${excepted.length} ${excepted.length === 1 ? 'policy' : 'policies'}`}
113
+ </span>
114
+ <span className="badge status-neutral">{isModern ? 'policies.kyverno.io' : 'kyverno.io'}</span>
115
+ </div>
116
+ {/* Kyverno has no expiry field on either shape — the upstream pattern
117
+ is a companion cleanup policy — so an exception persists until
118
+ somebody deletes it. */}
119
+ <div className="mt-2 text-xs text-theme-text-secondary">
120
+ Exceptions do not expire; this one applies until it is deleted.
121
+ </div>
122
+ </div>
123
+
124
+ {excepted.length > 0 && (
125
+ <Section title={`Excepted Policies (${excepted.length})`} icon={ShieldOff}>
126
+ <div className="space-y-2">
127
+ {excepted.map((policy, i) => (
128
+ <div key={i} className="card-inner">
129
+ <div className="flex items-center gap-2 flex-wrap">
130
+ <span className="text-sm font-medium text-theme-text-primary">{policy.name}</span>
131
+ {policy.kind && <span className="badge status-neutral">{policy.kind}</span>}
132
+ </div>
133
+ {policy.ruleNames.length > 0 ? (
134
+ <div className="mt-1 flex items-center gap-1 flex-wrap">
135
+ <span className="text-xs text-theme-text-tertiary">Rules:</span>
136
+ {policy.ruleNames.map((rule) => (
137
+ <span key={rule} className="badge status-neutral">
138
+ {rule}
139
+ </span>
140
+ ))}
141
+ </div>
142
+ ) : (
143
+ !isModern && <div className="mt-1 text-xs text-theme-text-tertiary">All rules in this policy</div>
144
+ )}
145
+ </div>
146
+ ))}
147
+ </div>
148
+ </Section>
149
+ )}
150
+
151
+ {reportResult && (
152
+ <Section title="Reporting" icon={ScrollText}>
153
+ <PropertyList>
154
+ <Property
155
+ label="Report Result"
156
+ value={
157
+ <span className="flex items-center gap-2">
158
+ <span className="badge status-neutral">{reportResult}</span>
159
+ <span className="text-xs text-theme-text-tertiary">how excepted resources appear in reports</span>
160
+ </span>
161
+ }
162
+ />
163
+ </PropertyList>
164
+ </Section>
165
+ )}
166
+
167
+ {legacyMatch && (
168
+ <Section title="Match Scope" icon={Filter}>
169
+ <LegacyMatchBlock match={legacyMatch} />
170
+ </Section>
171
+ )}
172
+
173
+ {matchConditions.length > 0 && (
174
+ <Section title={`Match Conditions (${matchConditions.length})`} icon={Filter}>
175
+ <KyvernoExpressionList items={matchConditions} />
176
+ </Section>
177
+ )}
178
+
179
+ {legacyConditions && (
180
+ <Section title="Conditions" icon={Filter} defaultExpanded={false}>
181
+ <pre className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap break-all">
182
+ {JSON.stringify(legacyConditions, null, 2)}
183
+ </pre>
184
+ </Section>
185
+ )}
186
+
187
+ <ConditionsSection conditions={data?.status?.conditions} />
188
+ </div>
189
+ )
190
+ }
191
+
192
+ /**
193
+ * CleanupPolicy / ClusterCleanupPolicy (kyverno.io/v2), deprecated in Kyverno
194
+ * 1.18 in favour of DeletingPolicy. Rendered because the installed base runs
195
+ * it today and "what deleted my resource?" is an expensive question to answer
196
+ * without it.
197
+ */
198
+ export function KyvernoCleanupPolicyRenderer({ data }: { data: any }) {
199
+ const schedule = getKyvernoCleanupSchedule(data)
200
+ const kinds = getKyvernoCleanupMatchedKinds(data)
201
+ const lastExecution = getKyvernoCleanupLastExecution(data)
202
+ const propagation = getKyvernoCleanupPropagationPolicy(data)
203
+ const match = getKyvernoCleanupMatch(data)
204
+ const exclude = getKyvernoCleanupExclude(data)
205
+ const conditions = getKyvernoCleanupConditions(data)
206
+
207
+ return (
208
+ <div className="space-y-4">
209
+ <div className="card-inner-lg">
210
+ <div className="flex items-center gap-2 flex-wrap">
211
+ <span className="text-xs uppercase tracking-wide text-theme-text-tertiary">Deletes</span>
212
+ <span className="badge status-alert">{kinds.length > 0 ? kinds.join(', ') : 'matched resources'}</span>
213
+ {schedule ? (
214
+ <span className="text-xs text-theme-text-secondary">
215
+ on schedule <span className="font-mono">{schedule}</span>
216
+ </span>
217
+ ) : (
218
+ <span className="text-xs text-theme-text-secondary">no schedule set — this policy will not run</span>
219
+ )}
220
+ </div>
221
+ <div className="mt-2 text-xs text-theme-text-secondary">
222
+ Deprecated in Kyverno 1.18; DeletingPolicy (policies.kyverno.io) is its replacement.
223
+ </div>
224
+ </div>
225
+
226
+ <Section title="Schedule" icon={Clock}>
227
+ <PropertyList>
228
+ <Property label="Cron" value={schedule ? <span className="font-mono">{schedule}</span> : 'Not set'} />
229
+ {propagation && <Property label="Deletion Propagation" value={propagation} />}
230
+ {lastExecution && <Property label="Last Execution" value={lastExecution} />}
231
+ </PropertyList>
232
+ </Section>
233
+
234
+ {match && (
235
+ <Section title="Match Scope" icon={Filter}>
236
+ <LegacyMatchBlock match={match} />
237
+ </Section>
238
+ )}
239
+
240
+ {exclude && (
241
+ <Section title="Excluded" icon={Filter} defaultExpanded={false}>
242
+ <LegacyMatchBlock match={exclude} />
243
+ </Section>
244
+ )}
245
+
246
+ {conditions ? (
247
+ <Section title="Deletion Conditions" icon={Trash2}>
248
+ <pre className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap break-all">
249
+ {JSON.stringify(conditions, null, 2)}
250
+ </pre>
251
+ </Section>
252
+ ) : (
253
+ <AlertBanner
254
+ variant="warning"
255
+ icon={Trash2}
256
+ title="No deletion conditions"
257
+ message="Every resource matched by this policy is deleted when the schedule fires."
258
+ />
259
+ )}
260
+
261
+ <ConditionsSection conditions={data?.status?.conditions} />
262
+ </div>
263
+ )
264
+ }