@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,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
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Shared building blocks for the modern Kyverno CEL policy family
|
|
2
|
+
// (policies.kyverno.io). Each of the five policy kinds gets its own thin
|
|
3
|
+
// renderer; everything they genuinely have in common lives here so the
|
|
4
|
+
// enforcement-posture answer can't drift between them.
|
|
5
|
+
|
|
6
|
+
import { Shield, Filter, Code, AlertTriangle } from 'lucide-react'
|
|
7
|
+
import { Section, PropertyList, Property, LabelSelectorDisplay, AlertBanner } from '../../ui/drawer-components'
|
|
8
|
+
import {
|
|
9
|
+
formatKyvernoResourceRule,
|
|
10
|
+
getKyvernoEnforcementPosture,
|
|
11
|
+
getKyvernoExcludeResourceRules,
|
|
12
|
+
getKyvernoFailurePolicy,
|
|
13
|
+
getKyvernoMatchConditions,
|
|
14
|
+
getKyvernoNamespaceSelector,
|
|
15
|
+
getKyvernoObjectSelector,
|
|
16
|
+
getKyvernoPolicyReady,
|
|
17
|
+
getKyvernoPolicyStatusMessage,
|
|
18
|
+
getKyvernoResourceRules,
|
|
19
|
+
getKyvernoVariables,
|
|
20
|
+
isKyvernoAdmissionEnabled,
|
|
21
|
+
isKyvernoBackgroundEnabled,
|
|
22
|
+
type KyvernoNamedExpression,
|
|
23
|
+
type KyvernoPolicyFamily,
|
|
24
|
+
} from '../resource-utils-kyverno-modern'
|
|
25
|
+
|
|
26
|
+
const POSTURE_TONE: Record<string, string> = {
|
|
27
|
+
healthy: 'status-healthy',
|
|
28
|
+
degraded: 'status-degraded',
|
|
29
|
+
alert: 'status-alert',
|
|
30
|
+
unhealthy: 'status-unhealthy',
|
|
31
|
+
neutral: 'status-neutral',
|
|
32
|
+
unknown: 'status-unknown',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The answer to the operator's first question — "does this block, and what
|
|
37
|
+
* does it match?" — rendered above everything else.
|
|
38
|
+
*
|
|
39
|
+
* It states the EFFECTIVE posture, not the declared one. A policy declaring
|
|
40
|
+
* `validationActions: [Deny]` with admission evaluation disabled blocks
|
|
41
|
+
* nothing, and showing it as a red "Deny" badge would report a cluster as
|
|
42
|
+
* protected when it isn't.
|
|
43
|
+
*/
|
|
44
|
+
export function KyvernoPostureHeader({
|
|
45
|
+
data,
|
|
46
|
+
family,
|
|
47
|
+
}: {
|
|
48
|
+
data: any
|
|
49
|
+
family: KyvernoPolicyFamily | undefined
|
|
50
|
+
}) {
|
|
51
|
+
const posture = getKyvernoEnforcementPosture(data, family)
|
|
52
|
+
const ready = getKyvernoPolicyReady(data)
|
|
53
|
+
const statusMessage = getKyvernoPolicyStatusMessage(data)
|
|
54
|
+
const rules = getKyvernoResourceRules(data)
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="space-y-2">
|
|
58
|
+
{/* A policy that failed to compile enforces nothing whatever its spec
|
|
59
|
+
says — the most common "why isn't my policy working" cause, so it
|
|
60
|
+
outranks the posture badge. */}
|
|
61
|
+
{ready === false && (
|
|
62
|
+
<AlertBanner
|
|
63
|
+
variant="error"
|
|
64
|
+
icon={AlertTriangle}
|
|
65
|
+
title="Policy not ready"
|
|
66
|
+
message={
|
|
67
|
+
statusMessage ||
|
|
68
|
+
'The controller has not accepted this policy. It is not being enforced — check the CEL expressions for compile errors.'
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
<div className="card-inner-lg">
|
|
74
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
75
|
+
<span className="text-xs uppercase tracking-wide text-theme-text-tertiary">Enforcement</span>
|
|
76
|
+
<span className={`badge ${POSTURE_TONE[posture.level] || 'status-unknown'}`}>{posture.label}</span>
|
|
77
|
+
{posture.summary && <span className="text-xs text-theme-text-secondary">{posture.summary}</span>}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{posture.note && <div className="mt-2 text-xs text-theme-text-secondary">{posture.note}</div>}
|
|
81
|
+
|
|
82
|
+
{rules.length > 0 && (
|
|
83
|
+
<div className="mt-3">
|
|
84
|
+
<div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">Applies to</div>
|
|
85
|
+
<div className="space-y-1">
|
|
86
|
+
{rules.map((rule, i) => (
|
|
87
|
+
<div key={i} className="text-xs font-mono text-theme-text-primary break-all">
|
|
88
|
+
{formatKyvernoResourceRule(rule)}
|
|
89
|
+
</div>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Match scope beyond the headline resourceRules — exclusions and selectors.
|
|
101
|
+
* The header already shows what the policy applies to; this section is the
|
|
102
|
+
* detail an operator opens when the headline doesn't explain why a given
|
|
103
|
+
* resource was or wasn't caught.
|
|
104
|
+
*/
|
|
105
|
+
export function KyvernoMatchScopeSection({ data }: { data: any }) {
|
|
106
|
+
const excludeRules = getKyvernoExcludeResourceRules(data)
|
|
107
|
+
const namespaceSelector = getKyvernoNamespaceSelector(data)
|
|
108
|
+
const objectSelector = getKyvernoObjectSelector(data)
|
|
109
|
+
const matchConditions = getKyvernoMatchConditions(data)
|
|
110
|
+
|
|
111
|
+
const hasSelectors =
|
|
112
|
+
(namespaceSelector && Object.keys(namespaceSelector).length > 0) ||
|
|
113
|
+
(objectSelector && Object.keys(objectSelector).length > 0)
|
|
114
|
+
|
|
115
|
+
if (excludeRules.length === 0 && !hasSelectors && matchConditions.length === 0) return null
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Section title="Match Scope" icon={Filter}>
|
|
119
|
+
<PropertyList>
|
|
120
|
+
{namespaceSelector && Object.keys(namespaceSelector).length > 0 && (
|
|
121
|
+
<Property label="Namespace Selector" value={<LabelSelectorDisplay selector={namespaceSelector} inline />} />
|
|
122
|
+
)}
|
|
123
|
+
{objectSelector && Object.keys(objectSelector).length > 0 && (
|
|
124
|
+
<Property label="Object Selector" value={<LabelSelectorDisplay selector={objectSelector} inline />} />
|
|
125
|
+
)}
|
|
126
|
+
</PropertyList>
|
|
127
|
+
|
|
128
|
+
{excludeRules.length > 0 && (
|
|
129
|
+
<div className="mt-3">
|
|
130
|
+
<div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">Excluded</div>
|
|
131
|
+
<div className="space-y-1">
|
|
132
|
+
{excludeRules.map((rule, i) => (
|
|
133
|
+
<div key={i} className="text-xs font-mono text-theme-text-secondary break-all">
|
|
134
|
+
{formatKyvernoResourceRule(rule)}
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{matchConditions.length > 0 && (
|
|
142
|
+
<div className="mt-3">
|
|
143
|
+
<div className="text-xs uppercase tracking-wide text-theme-text-tertiary mb-1">
|
|
144
|
+
Match Conditions ({matchConditions.length})
|
|
145
|
+
</div>
|
|
146
|
+
<KyvernoExpressionList items={matchConditions} />
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</Section>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A list of named CEL expressions in a mono block. */
|
|
154
|
+
export function KyvernoExpressionList({ items }: { items: KyvernoNamedExpression[] }) {
|
|
155
|
+
if (items.length === 0) return null
|
|
156
|
+
return (
|
|
157
|
+
<div className="space-y-2">
|
|
158
|
+
{items.map((item, i) => (
|
|
159
|
+
<div key={i} className="card-inner">
|
|
160
|
+
{item.name && <div className="text-xs font-medium text-theme-text-primary mb-1">{item.name}</div>}
|
|
161
|
+
<pre className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap break-all">
|
|
162
|
+
{item.expression}
|
|
163
|
+
</pre>
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function KyvernoVariablesSection({ data }: { data: any }) {
|
|
171
|
+
const variables = getKyvernoVariables(data)
|
|
172
|
+
if (variables.length === 0) return null
|
|
173
|
+
return (
|
|
174
|
+
<Section title={`Variables (${variables.length})`} icon={Code} defaultExpanded={false}>
|
|
175
|
+
<KyvernoExpressionList items={variables} />
|
|
176
|
+
</Section>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The evaluation/webhook knobs shared by the four admission-shaped kinds.
|
|
182
|
+
* DeletingPolicy has none of these — it is scheduled, not admission-driven —
|
|
183
|
+
* and deliberately does not render this section.
|
|
184
|
+
*/
|
|
185
|
+
export function KyvernoEvaluationSection({ data }: { data: any }) {
|
|
186
|
+
const failurePolicy = getKyvernoFailurePolicy(data)
|
|
187
|
+
return (
|
|
188
|
+
<Section title="Evaluation" icon={Shield} defaultExpanded={false}>
|
|
189
|
+
<PropertyList>
|
|
190
|
+
<Property label="Admission" value={isKyvernoAdmissionEnabled(data) ? 'Enabled' : 'Disabled'} />
|
|
191
|
+
<Property label="Background Scans" value={isKyvernoBackgroundEnabled(data) ? 'Enabled' : 'Disabled'} />
|
|
192
|
+
{failurePolicy && (
|
|
193
|
+
<Property
|
|
194
|
+
label="Failure Policy"
|
|
195
|
+
value={
|
|
196
|
+
<span className="flex items-center gap-2">
|
|
197
|
+
<span className={`badge ${failurePolicy === 'Ignore' ? 'status-degraded' : 'status-neutral'}`}>
|
|
198
|
+
{failurePolicy}
|
|
199
|
+
</span>
|
|
200
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
201
|
+
{failurePolicy === 'Ignore'
|
|
202
|
+
? 'requests are admitted when the engine itself errors'
|
|
203
|
+
: 'requests are rejected when the engine itself errors'}
|
|
204
|
+
</span>
|
|
205
|
+
</span>
|
|
206
|
+
}
|
|
207
|
+
/>
|
|
208
|
+
)}
|
|
209
|
+
</PropertyList>
|
|
210
|
+
</Section>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
@@ -16,7 +16,10 @@ export function OCIRepositoryRenderer({ data }: OCIRepositoryRendererProps) {
|
|
|
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 }> = []
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState, type ReactNode, type JSX } from 'react'
|
|
2
2
|
import { Server, HardDrive, Terminal as TerminalIcon, FileText, Activity, CirclePlay, FolderOpen, List, Eye, EyeOff, Shield } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
|
+
import { PolicySection } from './PolicySection'
|
|
5
|
+
import type { PolicyResourceResponse } from '../../../types/policy'
|
|
4
6
|
import { Section, PropertyList, Property, ConditionsSection, CopyHandler, AlertBanner, ResourceLink, useOperationalIssuesShown } from '../../ui/drawer-components'
|
|
5
7
|
import { formatResources, formatDuration, getPodProblems, getPodPhaseDisplay, healthColors, SEVERITY_DOT_COLOR, getDefaultContainerName } from '../resource-utils'
|
|
6
8
|
import { getResourceStatusColor, SEVERITY_BADGE_BORDERED } from '../../../utils/badge-colors'
|
|
@@ -108,6 +110,10 @@ interface PodRendererProps {
|
|
|
108
110
|
rbacData?: RBACSubjectResponse | null
|
|
109
111
|
rbacLoading?: boolean
|
|
110
112
|
rbacError?: Error | null
|
|
113
|
+
/** Undefined means the host didn't wire the fetch; the section is omitted. */
|
|
114
|
+
policyData?: PolicyResourceResponse | null
|
|
115
|
+
policyLoading?: boolean
|
|
116
|
+
policyError?: Error | null
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
// ── Env vars section — extracted to use hooks (useState for reveal) ──────────
|
|
@@ -287,6 +293,9 @@ export function PodRenderer({
|
|
|
287
293
|
rbacData,
|
|
288
294
|
rbacLoading,
|
|
289
295
|
rbacError,
|
|
296
|
+
policyData,
|
|
297
|
+
policyLoading,
|
|
298
|
+
policyError,
|
|
290
299
|
}: PodRendererProps) {
|
|
291
300
|
const containerStatuses = data.status?.containerStatuses || []
|
|
292
301
|
const containers = data.spec?.containers || []
|
|
@@ -582,6 +591,10 @@ export function PodRenderer({
|
|
|
582
591
|
)}
|
|
583
592
|
|
|
584
593
|
{/* Container Status */}
|
|
594
|
+
{policyData !== undefined && (
|
|
595
|
+
<PolicySection data={policyData} loading={policyLoading} error={policyError} />
|
|
596
|
+
)}
|
|
597
|
+
|
|
585
598
|
<Section title="Containers" icon={HardDrive} defaultExpanded>
|
|
586
599
|
<div className="space-y-3">
|
|
587
600
|
{containers.map((container: any) => {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { PolicySection } from './PolicySection'
|
|
4
|
+
import type { PolicyResourceResponse } from '../../../types/policy'
|
|
5
|
+
|
|
6
|
+
const base: PolicyResourceResponse = {
|
|
7
|
+
evaluated: true,
|
|
8
|
+
status: 'ready',
|
|
9
|
+
liveUpdates: true,
|
|
10
|
+
counts: { pass: 0, fail: 0, warn: 0, error: 0, skip: 0 },
|
|
11
|
+
findings: [],
|
|
12
|
+
}
|
|
13
|
+
const resp = (over: Partial<PolicyResourceResponse>): PolicyResourceResponse => ({ ...base, ...over })
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// The unhappy paths carry this section. An empty finding list means several
|
|
17
|
+
// unrelated things, and rendering them all as blank space would tell an
|
|
18
|
+
// operator they are compliant in the cases where nothing was checked.
|
|
19
|
+
describe('PolicySection — what an empty result means', () => {
|
|
20
|
+
it('says which rules failed when there are violations', () => {
|
|
21
|
+
const html = renderToString(<PolicySection data={resp({
|
|
22
|
+
counts: { pass: 2, fail: 1, warn: 0, error: 0, skip: 0 },
|
|
23
|
+
findings: [{ policy: 'require-run-as-nonroot', result: 'fail', message: 'must set runAsNonRoot' }],
|
|
24
|
+
})} />)
|
|
25
|
+
expect(html).toContain('require-run-as-nonroot')
|
|
26
|
+
expect(html).toContain('must set runAsNonRoot')
|
|
27
|
+
// The passing checks are counted, not listed — otherwise two failures drown
|
|
28
|
+
// in a wall of green on a heavily-policed workload.
|
|
29
|
+
expect(html).toContain('2 other checks passing')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('distinguishes "everything passed" from "nothing was checked"', () => {
|
|
33
|
+
const passing = renderToString(<PolicySection data={resp({
|
|
34
|
+
counts: { pass: 5, fail: 0, warn: 0, error: 0, skip: 0 },
|
|
35
|
+
})} />)
|
|
36
|
+
expect(passing).toContain('All 5 checks passing')
|
|
37
|
+
|
|
38
|
+
const unchecked = renderToString(<PolicySection data={resp({})} />)
|
|
39
|
+
expect(unchecked).toContain('No policy applies')
|
|
40
|
+
expect(unchecked).not.toContain('passing')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('never claims compliance when the results could not be read', () => {
|
|
44
|
+
const html = renderToString(<PolicySection data={resp({
|
|
45
|
+
evaluated: false, status: 'deferred', reasonCode: 'rbac_denied',
|
|
46
|
+
})} />)
|
|
47
|
+
expect(html).toContain('has not been checked')
|
|
48
|
+
expect(html).toContain('rbac_denied')
|
|
49
|
+
expect(html).not.toContain('passing')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('says results are still loading during warmup rather than reporting clean', () => {
|
|
53
|
+
const html = renderToString(<PolicySection data={resp({ evaluated: false, status: 'warmup' })} />)
|
|
54
|
+
expect(html).toContain('still loading')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders nothing at all when no policy engine is installed', () => {
|
|
58
|
+
// A disclaimer on every cluster without a policy engine is pure noise.
|
|
59
|
+
expect(renderToString(<PolicySection data={resp({ evaluated: false, status: 'not_installed' })} />)).toBe('')
|
|
60
|
+
expect(renderToString(<PolicySection data={null} />)).toBe('')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('PolicySection — partial coverage', () => {
|
|
65
|
+
it('admits when some report families were unreadable, even while passing', () => {
|
|
66
|
+
// "All checks passing" plus an unreadable family is a claim of coverage we
|
|
67
|
+
// do not have.
|
|
68
|
+
const html = renderToString(<PolicySection data={resp({
|
|
69
|
+
counts: { pass: 3, fail: 0, warn: 0, error: 0, skip: 0 },
|
|
70
|
+
deniedGroups: ['openreports.io'],
|
|
71
|
+
})} />)
|
|
72
|
+
expect(html).toContain('All 3 checks passing')
|
|
73
|
+
expect(html).toContain('may be incomplete')
|
|
74
|
+
expect(html).toContain('openreports.io')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('admits when the results are frozen', () => {
|
|
78
|
+
const html = renderToString(<PolicySection data={resp({
|
|
79
|
+
counts: { pass: 1, fail: 0, warn: 0, error: 0, skip: 0 }, liveUpdates: false,
|
|
80
|
+
})} />)
|
|
81
|
+
expect(html).toContain('not updating live')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('PolicySection — request states', () => {
|
|
86
|
+
it('shows loading rather than an empty result', () => {
|
|
87
|
+
expect(renderToString(<PolicySection data={null} loading />)).toContain('Loading policy results')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('treats a permission denial as an expected state, not a red failure', () => {
|
|
91
|
+
const denied = Object.assign(new Error('forbidden'), { status: 403 })
|
|
92
|
+
const html = renderToString(<PolicySection data={null} error={denied} />)
|
|
93
|
+
expect(html).toContain('don’t have permission')
|
|
94
|
+
expect(html).not.toContain('text-red')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('keeps a genuine fault loud', () => {
|
|
98
|
+
const html = renderToString(<PolicySection data={null} error={new Error('connection reset')} />)
|
|
99
|
+
expect(html).toContain('connection reset')
|
|
100
|
+
expect(html).toContain('text-red')
|
|
101
|
+
})
|
|
102
|
+
})
|