@skyhook-io/k8s-ui 1.12.0 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/components/applications/ApplicationDetail.tsx +2 -2
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +32 -7
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +419 -9
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +47 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +59 -2
- package/src/components/resources/renderers/PolicySection.tsx +32 -5
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +54 -0
- package/src/components/resources/resource-utils-cnpg.ts +264 -3
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +13 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/SelectMenu.tsx +202 -28
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +5 -2
- package/src/types/core.ts +36 -4
- package/src/types/policy.ts +153 -2
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +50 -0
- package/src/utils/extended-resources.ts +4 -7
- package/src/utils/format.test.ts +83 -1
- package/src/utils/format.ts +80 -1
- package/src/utils/inClusterConsent.test.ts +9 -0
- package/src/utils/inClusterConsent.ts +9 -9
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- package/src/utils/resource-icons.ts +29 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type React from 'react'
|
|
1
2
|
// Renderers for Kyverno's modern CEL policy family (policies.kyverno.io),
|
|
2
3
|
// stabilized in Kyverno 1.17/1.18 and the family that survives the removal of
|
|
3
4
|
// kyverno.io Policy/ClusterPolicy planned for 1.20.
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
|
|
15
16
|
import { FileWarning, ShieldCheck, Wand2, Sparkles, Trash2, Clock, Fingerprint, ScrollText } from 'lucide-react'
|
|
16
17
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
18
|
+
import { CronValue } from '../../ui/ScheduleValue'
|
|
17
19
|
import {
|
|
18
20
|
KyvernoEvaluationSection,
|
|
19
21
|
KyvernoExpressionList,
|
|
@@ -70,10 +72,11 @@ function ValidationsSection({ validations }: { validations: KyvernoCELValidation
|
|
|
70
72
|
)
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
export function KyvernoValidatingPolicyRenderer({ data }: { data: any }) {
|
|
75
|
+
export function KyvernoValidatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
74
76
|
return (
|
|
75
77
|
<div className="space-y-4">
|
|
76
78
|
<KyvernoPostureHeader data={data} family="validating" />
|
|
79
|
+
{coverage}
|
|
77
80
|
<ValidationsSection validations={getKyvernoValidations(data)} />
|
|
78
81
|
<KyvernoMatchScopeSection data={data} />
|
|
79
82
|
<KyvernoVariablesSection data={data} />
|
|
@@ -83,7 +86,7 @@ export function KyvernoValidatingPolicyRenderer({ data }: { data: any }) {
|
|
|
83
86
|
)
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
|
|
89
|
+
export function KyvernoImageValidatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
87
90
|
const attestors = getKyvernoAttestors(data)
|
|
88
91
|
const attestations = getKyvernoAttestations(data)
|
|
89
92
|
const imageRefs = getKyvernoImageReferences(data)
|
|
@@ -92,6 +95,7 @@ export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
|
|
|
92
95
|
return (
|
|
93
96
|
<div className="space-y-4">
|
|
94
97
|
<KyvernoPostureHeader data={data} family="imageValidating" />
|
|
98
|
+
{coverage}
|
|
95
99
|
|
|
96
100
|
{imageRefs.length > 0 && (
|
|
97
101
|
<Section title={`Image References (${imageRefs.length})`} icon={ScrollText}>
|
|
@@ -163,13 +167,14 @@ export function KyvernoImageValidatingPolicyRenderer({ data }: { data: any }) {
|
|
|
163
167
|
)
|
|
164
168
|
}
|
|
165
169
|
|
|
166
|
-
export function KyvernoMutatingPolicyRenderer({ data }: { data: any }) {
|
|
170
|
+
export function KyvernoMutatingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
167
171
|
const mutations = getKyvernoMutations(data)
|
|
168
172
|
const reinvocation = getKyvernoReinvocationPolicy(data)
|
|
169
173
|
|
|
170
174
|
return (
|
|
171
175
|
<div className="space-y-4">
|
|
172
176
|
<KyvernoPostureHeader data={data} family="mutating" />
|
|
177
|
+
{coverage}
|
|
173
178
|
|
|
174
179
|
{mutations.length > 0 && (
|
|
175
180
|
<Section title={`Mutations (${mutations.length})`} icon={Wand2}>
|
|
@@ -218,13 +223,14 @@ export function KyvernoMutatingPolicyRenderer({ data }: { data: any }) {
|
|
|
218
223
|
)
|
|
219
224
|
}
|
|
220
225
|
|
|
221
|
-
export function KyvernoGeneratingPolicyRenderer({ data }: { data: any }) {
|
|
226
|
+
export function KyvernoGeneratingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
222
227
|
const generations = getKyvernoGenerations(data)
|
|
223
228
|
const settings = getKyvernoGenerationSettings(data)
|
|
224
229
|
|
|
225
230
|
return (
|
|
226
231
|
<div className="space-y-4">
|
|
227
232
|
<KyvernoPostureHeader data={data} family="generating" />
|
|
233
|
+
{coverage}
|
|
228
234
|
|
|
229
235
|
{generations.length > 0 && (
|
|
230
236
|
<Section title={`Generates (${generations.length})`} icon={Sparkles}>
|
|
@@ -274,7 +280,7 @@ export function KyvernoGeneratingPolicyRenderer({ data }: { data: any }) {
|
|
|
274
280
|
)
|
|
275
281
|
}
|
|
276
282
|
|
|
277
|
-
export function KyvernoDeletingPolicyRenderer({ data }: { data: any }) {
|
|
283
|
+
export function KyvernoDeletingPolicyRenderer({ data, coverage }: { data: any; coverage?: React.ReactNode }) {
|
|
278
284
|
const schedule = getKyvernoSchedule(data)
|
|
279
285
|
const conditions = getKyvernoDeletionConditions(data)
|
|
280
286
|
const propagation = getKyvernoDeletionPropagationPolicy(data)
|
|
@@ -282,12 +288,13 @@ export function KyvernoDeletingPolicyRenderer({ data }: { data: any }) {
|
|
|
282
288
|
return (
|
|
283
289
|
<div className="space-y-4">
|
|
284
290
|
<KyvernoPostureHeader data={data} family="deleting" />
|
|
291
|
+
{coverage}
|
|
285
292
|
|
|
286
293
|
<Section title="Schedule" icon={Clock}>
|
|
287
294
|
<PropertyList>
|
|
288
295
|
<Property
|
|
289
296
|
label="Cron"
|
|
290
|
-
value={schedule ? <
|
|
297
|
+
value={schedule ? <CronValue cron={schedule} /> : 'Not set — this policy will not run'}
|
|
291
298
|
/>
|
|
292
299
|
{propagation && <Property label="Deletion Propagation" value={propagation} />}
|
|
293
300
|
</PropertyList>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
AlertBanner,
|
|
15
15
|
LabelSelectorDisplay,
|
|
16
16
|
} from '../../ui/drawer-components'
|
|
17
|
+
import { CronValue, TimeValue } from '../../ui/ScheduleValue'
|
|
17
18
|
import { KyvernoExpressionList } from './KyvernoPolicyShared'
|
|
18
19
|
import {
|
|
19
20
|
getKyvernoCleanupConditions,
|
|
@@ -225,9 +226,9 @@ export function KyvernoCleanupPolicyRenderer({ data }: { data: any }) {
|
|
|
225
226
|
|
|
226
227
|
<Section title="Schedule" icon={Clock}>
|
|
227
228
|
<PropertyList>
|
|
228
|
-
<Property label="Cron" value={schedule ? <
|
|
229
|
+
<Property label="Cron" value={schedule ? <CronValue cron={schedule} /> : 'Not set'} />
|
|
229
230
|
{propagation && <Property label="Deletion Propagation" value={propagation} />}
|
|
230
|
-
{lastExecution && <Property label="Last Execution" value={lastExecution} />}
|
|
231
|
+
{lastExecution && <Property label="Last Execution" value={<TimeValue timestamp={lastExecution} />} />}
|
|
231
232
|
</PropertyList>
|
|
232
233
|
</Section>
|
|
233
234
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getKyvernoContextRefresh } from './KyvernoGlobalContextRenderer'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kyverno records a successful refresh and records nothing at all for a failed
|
|
6
|
+
* one — no message, no condition. Verified on 1.18.2: `cluster-namespaces` came
|
|
7
|
+
* back with `lastRefreshTime`, `broken-lookup` came back with an empty status.
|
|
8
|
+
* Age is therefore the only thing separating "failing" from "not yet".
|
|
9
|
+
*/
|
|
10
|
+
describe('global context refresh state', () => {
|
|
11
|
+
const ago = (ms: number) => new Date(Date.now() - ms).toISOString()
|
|
12
|
+
|
|
13
|
+
it('is healthy once it has refreshed', () => {
|
|
14
|
+
const r = getKyvernoContextRefresh({ status: { lastRefreshTime: ago(60_000) } })
|
|
15
|
+
expect(r.refreshed).toBe(true)
|
|
16
|
+
expect(r.level).toBe('healthy')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('is unknown while the entry is young enough not to have run yet', () => {
|
|
20
|
+
const r = getKyvernoContextRefresh({ metadata: { creationTimestamp: ago(10_000) }, status: {} })
|
|
21
|
+
expect(r.level).toBe('unknown')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Old and never refreshed is the diagnosis; anything sooner would accuse the
|
|
25
|
+
// entry before its first attempt.
|
|
26
|
+
it('is unhealthy once an old entry still has no refresh', () => {
|
|
27
|
+
const r = getKyvernoContextRefresh({ metadata: { creationTimestamp: ago(10 * 60_000) }, status: {} })
|
|
28
|
+
expect(r.level).toBe('unhealthy')
|
|
29
|
+
expect(r.refreshed).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Globe, RefreshCw } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { HEALTH_BADGE_COLORS } from '../../../utils/badge-colors'
|
|
5
|
+
import { formatAge, healthColors } from '../resource-utils'
|
|
6
|
+
import type { StatusBadge } from '../resource-utils'
|
|
7
|
+
|
|
8
|
+
type Nav = (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether the entry has ever produced data.
|
|
12
|
+
*
|
|
13
|
+
* Kyverno records success as `status.lastRefreshTime` and records failure as
|
|
14
|
+
* nothing at all — there is no message, no condition, no reason. So the only
|
|
15
|
+
* honest reading is "it has refreshed" versus "it has never refreshed", and the
|
|
16
|
+
* age of the entry is what turns the second into a diagnosis. Inventing a cause
|
|
17
|
+
* for a silent failure would be worse than naming the silence.
|
|
18
|
+
*/
|
|
19
|
+
export function getKyvernoContextRefresh(resource: any): {
|
|
20
|
+
refreshed: boolean
|
|
21
|
+
at?: string
|
|
22
|
+
level: 'healthy' | 'unhealthy' | 'unknown'
|
|
23
|
+
} {
|
|
24
|
+
const at = resource?.status?.lastRefreshTime
|
|
25
|
+
if (typeof at === 'string' && at !== '') return { refreshed: true, at, level: 'healthy' }
|
|
26
|
+
// Young enough that the first refresh may simply not have happened yet.
|
|
27
|
+
const created = resource?.metadata?.creationTimestamp
|
|
28
|
+
const ageMs = created ? Date.now() - new Date(created).getTime() : 0
|
|
29
|
+
return { refreshed: false, level: ageMs > 2 * 60 * 1000 ? 'unhealthy' : 'unknown' }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GlobalContextEntry — external data a policy reads while it evaluates.
|
|
34
|
+
*
|
|
35
|
+
* It matters because a policy referencing an entry that never resolves fails at
|
|
36
|
+
* evaluation time, and the failure surfaces as a policy error with no obvious
|
|
37
|
+
* cause. This page is where that cause lives.
|
|
38
|
+
*/
|
|
39
|
+
export function KyvernoGlobalContextRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
|
|
40
|
+
const spec = data?.spec ?? {}
|
|
41
|
+
const api = spec.apiCall
|
|
42
|
+
const cm = spec.configMap
|
|
43
|
+
const refresh = getKyvernoContextRefresh(data)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
{refresh.level === 'unhealthy' && (
|
|
48
|
+
<AlertBanner
|
|
49
|
+
variant="error"
|
|
50
|
+
title="This entry has never refreshed"
|
|
51
|
+
// Deliberately not a diagnosis: Kyverno records no reason for a failed
|
|
52
|
+
// refresh, so the page reports the fact and where to look.
|
|
53
|
+
message="Kyverno records no reason when a refresh fails. Any policy referencing this entry will error while it evaluates. Check the source below is reachable and that Kyverno may read it."
|
|
54
|
+
/>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
<Section title="Refresh" icon={RefreshCw} defaultExpanded>
|
|
58
|
+
<PropertyList>
|
|
59
|
+
<Property
|
|
60
|
+
label="State"
|
|
61
|
+
value={
|
|
62
|
+
<span
|
|
63
|
+
className={clsx('badge', HEALTH_BADGE_COLORS[refresh.level as keyof typeof HEALTH_BADGE_COLORS])}
|
|
64
|
+
>
|
|
65
|
+
{refresh.refreshed ? 'Refreshed' : refresh.level === 'unhealthy' ? 'Never refreshed' : 'Not refreshed yet'}
|
|
66
|
+
</span>
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
{refresh.at && <Property label="Last Refreshed" value={`${formatAge(refresh.at)} ago`} />}
|
|
70
|
+
{api?.refreshInterval && <Property label="Interval" value={api.refreshInterval} />}
|
|
71
|
+
{api?.retryLimit != null && <Property label="Retry Limit" value={String(api.retryLimit)} />}
|
|
72
|
+
</PropertyList>
|
|
73
|
+
{!refresh.refreshed && refresh.level === 'unknown' && (
|
|
74
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">
|
|
75
|
+
Created moments ago — the first refresh may not have run yet.
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</Section>
|
|
79
|
+
|
|
80
|
+
<Section title="Source" icon={Globe} defaultExpanded>
|
|
81
|
+
{api ? (
|
|
82
|
+
<PropertyList>
|
|
83
|
+
<Property label="Kind" value="Kubernetes API call" />
|
|
84
|
+
<Property label="Method" value={api.method || 'GET'} />
|
|
85
|
+
<Property label="Path" value={<span className="font-mono text-xs">{api.urlPath || '-'}</span>} />
|
|
86
|
+
{api.jmesPath && (
|
|
87
|
+
<Property label="Filter" value={<span className="font-mono text-xs">{api.jmesPath}</span>} />
|
|
88
|
+
)}
|
|
89
|
+
</PropertyList>
|
|
90
|
+
) : cm ? (
|
|
91
|
+
<PropertyList>
|
|
92
|
+
<Property label="Kind" value="ConfigMap" />
|
|
93
|
+
<Property
|
|
94
|
+
label="ConfigMap"
|
|
95
|
+
value={
|
|
96
|
+
<ResourceLink
|
|
97
|
+
name={cm.name}
|
|
98
|
+
kind="configmaps"
|
|
99
|
+
namespace={cm.namespace ?? ''}
|
|
100
|
+
onNavigate={onNavigate}
|
|
101
|
+
/>
|
|
102
|
+
}
|
|
103
|
+
/>
|
|
104
|
+
{cm.namespace && <Property label="Namespace" value={cm.namespace} />}
|
|
105
|
+
</PropertyList>
|
|
106
|
+
) : (
|
|
107
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
108
|
+
No API call or ConfigMap source is declared, so this entry can never produce data.
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</Section>
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** List badge for GlobalContextEntry, mirroring the detail page's three states. */
|
|
117
|
+
export function getKyvernoGlobalContextStatus(resource: any): StatusBadge {
|
|
118
|
+
const r = getKyvernoContextRefresh(resource)
|
|
119
|
+
if (r.refreshed) return { text: 'Refreshed', color: healthColors.healthy, level: 'healthy' }
|
|
120
|
+
if (r.level === 'unhealthy') {
|
|
121
|
+
return { text: 'Never Refreshed', color: healthColors.unhealthy, level: 'unhealthy' }
|
|
122
|
+
}
|
|
123
|
+
return { text: 'Pending', color: healthColors.unknown, level: 'unknown' }
|
|
124
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type React from 'react'
|
|
1
2
|
import { Shield, ShieldCheck, ShieldAlert, FileWarning, ListChecks, ChevronDown, ChevronRight } from 'lucide-react'
|
|
2
3
|
import { clsx } from 'clsx'
|
|
3
4
|
import { useState } from 'react'
|
|
@@ -225,6 +226,13 @@ export function PolicyReportRenderer({ data }: PolicyReportRendererProps) {
|
|
|
225
226
|
|
|
226
227
|
interface KyvernoPolicyRendererProps {
|
|
227
228
|
data: any
|
|
229
|
+
/** Filled by the host with the policy's coverage section — what this policy
|
|
230
|
+
* actually decided about the cluster. */
|
|
231
|
+
coverage?: React.ReactNode
|
|
232
|
+
/** Filled by the host with the work this policy has queued and not finished.
|
|
233
|
+
* The failure worth catching here is a pile-up, not one request, so it
|
|
234
|
+
* belongs beside the policy rather than only on its own page. */
|
|
235
|
+
queued?: React.ReactNode
|
|
228
236
|
}
|
|
229
237
|
|
|
230
238
|
const ruleTypeColorMap: Record<string, string> = {
|
|
@@ -234,7 +242,7 @@ const ruleTypeColorMap: Record<string, string> = {
|
|
|
234
242
|
verifyImages: 'bg-orange-500/20 text-orange-400',
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
245
|
+
export function KyvernoPolicyRenderer({ data, coverage, queued }: KyvernoPolicyRendererProps) {
|
|
238
246
|
const spec = data.spec || {}
|
|
239
247
|
const status = data.status || {}
|
|
240
248
|
const conditions = status.conditions || []
|
|
@@ -249,9 +257,18 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
|
249
257
|
|
|
250
258
|
return (
|
|
251
259
|
<>
|
|
260
|
+
{coverage}
|
|
261
|
+
{queued}
|
|
252
262
|
{/* Configuration */}
|
|
253
263
|
<Section title="Configuration" icon={Shield}>
|
|
254
264
|
<PropertyList>
|
|
265
|
+
{/* Audit / Enforce governs the rules that can REJECT — validation and
|
|
266
|
+
image verification. A policy whose rules only generate or mutate
|
|
267
|
+
carries the field inertly, and showing it as the policy's mode
|
|
268
|
+
reads as "this one only reports" about a policy that is actively
|
|
269
|
+
writing to the cluster. status.rulecount is Kyverno's own count,
|
|
270
|
+
so this is checkable rather than guessed. */}
|
|
271
|
+
{(ruleCountByType.validate > 0 || ruleCountByType.verifyImages > 0) && (
|
|
255
272
|
<Property label="Failure Action" value={
|
|
256
273
|
<span className={clsx(
|
|
257
274
|
'badge',
|
|
@@ -260,6 +277,7 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
|
260
277
|
{action}
|
|
261
278
|
</span>
|
|
262
279
|
} />
|
|
280
|
+
)}
|
|
263
281
|
<Property label="Background" value={background ? 'Enabled' : 'Disabled'} />
|
|
264
282
|
{spec.webhookTimeoutSeconds && (
|
|
265
283
|
<Property label="Webhook Timeout" value={`${spec.webhookTimeoutSeconds}s`} />
|
|
@@ -342,7 +360,15 @@ export function KyvernoPolicyRenderer({ data }: KyvernoPolicyRendererProps) {
|
|
|
342
360
|
{status.rulecount.validate !== undefined && <Property label="Validate" value={status.rulecount.validate} />}
|
|
343
361
|
{status.rulecount.mutate !== undefined && <Property label="Mutate" value={status.rulecount.mutate} />}
|
|
344
362
|
{status.rulecount.generate !== undefined && <Property label="Generate" value={status.rulecount.generate} />}
|
|
345
|
-
{
|
|
363
|
+
{/* All-lowercase in Kyverno's own status, so the camelCase read
|
|
364
|
+
dropped the only non-zero row on an image-verification policy
|
|
365
|
+
and left three zeros reading as "no rules". */}
|
|
366
|
+
{(status.rulecount.verifyImages ?? status.rulecount.verifyimages) !== undefined && (
|
|
367
|
+
<Property
|
|
368
|
+
label="Verify Images"
|
|
369
|
+
value={status.rulecount.verifyImages ?? status.rulecount.verifyimages}
|
|
370
|
+
/>
|
|
371
|
+
)}
|
|
346
372
|
</PropertyList>
|
|
347
373
|
</Section>
|
|
348
374
|
)}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { ListChecks, Workflow, Boxes, FileWarning } from 'lucide-react'
|
|
4
|
+
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
5
|
+
import { HEALTH_BADGE_COLORS, SEVERITY_BADGE } from '../../../utils/badge-colors'
|
|
6
|
+
import { formatAge } from '../resource-utils'
|
|
7
|
+
import {
|
|
8
|
+
getKyvernoRequestType,
|
|
9
|
+
getKyvernoRequestTriggers,
|
|
10
|
+
getKyvernoRequestGenerated,
|
|
11
|
+
getKyvernoRequestState,
|
|
12
|
+
getKyvernoRequestPolicy,
|
|
13
|
+
getKyvernoRequestMessage,
|
|
14
|
+
getKyvernoReportSubject,
|
|
15
|
+
getKyvernoReportSource,
|
|
16
|
+
getKyvernoReportSummary,
|
|
17
|
+
getKyvernoReportResults,
|
|
18
|
+
kyvernoReportTimestamp,
|
|
19
|
+
} from '../resource-utils-kyverno-queue'
|
|
20
|
+
|
|
21
|
+
type Nav = (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
22
|
+
|
|
23
|
+
/** Long trigger lists fold at the same point as the house relationship lists. */
|
|
24
|
+
const FOLD = 10
|
|
25
|
+
|
|
26
|
+
function RefList({
|
|
27
|
+
refs,
|
|
28
|
+
onNavigate,
|
|
29
|
+
empty,
|
|
30
|
+
}: {
|
|
31
|
+
refs: Array<{ apiVersion?: string; kind?: string; name?: string; namespace?: string }>
|
|
32
|
+
onNavigate?: Nav
|
|
33
|
+
empty: string
|
|
34
|
+
}) {
|
|
35
|
+
const [expanded, setExpanded] = useState(false)
|
|
36
|
+
if (refs.length === 0) return <div className="text-sm text-theme-text-tertiary">{empty}</div>
|
|
37
|
+
|
|
38
|
+
const shown = expanded ? refs : refs.slice(0, FOLD)
|
|
39
|
+
return (
|
|
40
|
+
<div className="space-y-1.5">
|
|
41
|
+
<div className="flex flex-wrap gap-1.5">
|
|
42
|
+
{shown.map((r, i) => (
|
|
43
|
+
<ResourceLink
|
|
44
|
+
key={`${r.kind}/${r.namespace}/${r.name}/${i}`}
|
|
45
|
+
name={r.name ?? ''}
|
|
46
|
+
kind={r.kind ?? ''}
|
|
47
|
+
namespace={r.namespace ?? ''}
|
|
48
|
+
group={r.apiVersion?.includes('/') ? r.apiVersion.split('/')[0] : undefined}
|
|
49
|
+
onNavigate={onNavigate}
|
|
50
|
+
/>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
{refs.length > FOLD && (
|
|
54
|
+
<button
|
|
55
|
+
className="text-xs text-theme-text-secondary hover:text-theme-text-primary"
|
|
56
|
+
onClick={() => setExpanded(!expanded)}
|
|
57
|
+
>
|
|
58
|
+
{expanded ? 'Show fewer' : `Show all ${refs.length}`}
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* UpdateRequest — the record of work Kyverno queued rather than did inline.
|
|
67
|
+
*
|
|
68
|
+
* It is where a generation or a mutate-existing that silently stopped is
|
|
69
|
+
* diagnosed: the policy looks Ready, the target never appears, and the only
|
|
70
|
+
* evidence is a request sitting in Pending.
|
|
71
|
+
*
|
|
72
|
+
* The two request types are shaped so differently that one renderer reading a
|
|
73
|
+
* single field set would be blank for half of them — see the accessors.
|
|
74
|
+
*/
|
|
75
|
+
export function KyvernoUpdateRequestRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
|
|
76
|
+
const type = getKyvernoRequestType(data)
|
|
77
|
+
const state = getKyvernoRequestState(data)
|
|
78
|
+
const triggers = getKyvernoRequestTriggers(data)
|
|
79
|
+
const generated = getKyvernoRequestGenerated(data)
|
|
80
|
+
const policy = getKyvernoRequestPolicy(data)
|
|
81
|
+
const message = getKyvernoRequestMessage(data)
|
|
82
|
+
// Every trigger in one request comes from the same rule; a generate request
|
|
83
|
+
// repeats it per entry, a mutate request states it once.
|
|
84
|
+
const rule = data?.spec?.rule || triggers.find((t) => t.rule)?.rule
|
|
85
|
+
const deletesDownstream = data?.spec?.deleteDownstream === true || triggers.some((t) => t.deleteDownstream)
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<>
|
|
89
|
+
{state.level === 'unhealthy' && (
|
|
90
|
+
<AlertBanner
|
|
91
|
+
variant="error"
|
|
92
|
+
title="This request failed"
|
|
93
|
+
message={message || 'Kyverno could not complete this request. The resources below were not updated.'}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
{state.level === 'degraded' && (
|
|
97
|
+
<AlertBanner
|
|
98
|
+
variant="warning"
|
|
99
|
+
title="Still queued"
|
|
100
|
+
// Pending is normal for seconds and a symptom for longer, and this
|
|
101
|
+
// object carries no timestamp of its own beyond creation.
|
|
102
|
+
message="Kyverno has not processed this request yet. If it stays here, the background controller is not running or cannot reach what it needs."
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<Section title="Request" icon={Workflow} defaultExpanded>
|
|
107
|
+
<PropertyList>
|
|
108
|
+
<Property
|
|
109
|
+
label="State"
|
|
110
|
+
value={
|
|
111
|
+
<span className={clsx('badge', HEALTH_BADGE_COLORS[state.level as keyof typeof HEALTH_BADGE_COLORS])}>
|
|
112
|
+
{state.text}
|
|
113
|
+
</span>
|
|
114
|
+
}
|
|
115
|
+
/>
|
|
116
|
+
<Property label="Type" value={type === 'unknown' ? '-' : type} />
|
|
117
|
+
{policy && (
|
|
118
|
+
<Property
|
|
119
|
+
label="Policy"
|
|
120
|
+
value={
|
|
121
|
+
// `spec.policy` is a bare name with no kind and no namespace, and
|
|
122
|
+
// a namespaced Policy may share it with a ClusterPolicy. A
|
|
123
|
+
// qualified name is the one case that identifies itself; anything
|
|
124
|
+
// else links to a page that may not be the policy that queued
|
|
125
|
+
// this, so it stays plain text rather than guessing.
|
|
126
|
+
policy.includes('/') ? (
|
|
127
|
+
<ResourceLink
|
|
128
|
+
name={policy.split('/')[1]}
|
|
129
|
+
// `policies` is what the API serves and what a URL can be
|
|
130
|
+
// built from; `kyvernopolicies` is an internal display alias
|
|
131
|
+
// and 400s when asked for. The group is what disambiguates.
|
|
132
|
+
kind="policies"
|
|
133
|
+
namespace={policy.split('/')[0]}
|
|
134
|
+
group="kyverno.io"
|
|
135
|
+
label={policy}
|
|
136
|
+
onNavigate={onNavigate}
|
|
137
|
+
/>
|
|
138
|
+
) : (
|
|
139
|
+
policy
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
{rule && <Property label="Rule" value={rule} />}
|
|
145
|
+
<Property
|
|
146
|
+
label="On trigger delete"
|
|
147
|
+
value={
|
|
148
|
+
deletesDownstream ? (
|
|
149
|
+
<span className="text-warning-text">
|
|
150
|
+
Removes what this rule created for it
|
|
151
|
+
</span>
|
|
152
|
+
) : (
|
|
153
|
+
'Leaves what this rule created in place'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
/>
|
|
157
|
+
{data?.metadata?.creationTimestamp && (
|
|
158
|
+
<Property label="Queued" value={`${formatAge(data.metadata.creationTimestamp)} ago`} />
|
|
159
|
+
)}
|
|
160
|
+
</PropertyList>
|
|
161
|
+
{message && state.level !== 'unhealthy' && (
|
|
162
|
+
<div className="mt-2 pt-2 border-t border-theme-border text-xs text-theme-text-secondary">{message}</div>
|
|
163
|
+
)}
|
|
164
|
+
</Section>
|
|
165
|
+
|
|
166
|
+
<Section title={`Triggered By (${triggers.length})`} icon={ListChecks} defaultExpanded>
|
|
167
|
+
<RefList
|
|
168
|
+
refs={triggers}
|
|
169
|
+
onNavigate={onNavigate}
|
|
170
|
+
// A queued generate request has not been given its triggers yet, so
|
|
171
|
+
// the empty list means "not written down", not "acts on nothing".
|
|
172
|
+
empty={
|
|
173
|
+
state.level === 'degraded'
|
|
174
|
+
? 'Kyverno has not recorded what this request is for yet.'
|
|
175
|
+
: 'This request names no trigger, so there is nothing for it to act on.'
|
|
176
|
+
}
|
|
177
|
+
/>
|
|
178
|
+
</Section>
|
|
179
|
+
|
|
180
|
+
{type === 'generate' && (
|
|
181
|
+
<Section title={`Created (${generated.length})`} icon={Boxes} defaultExpanded>
|
|
182
|
+
<RefList
|
|
183
|
+
refs={generated}
|
|
184
|
+
onNavigate={onNavigate}
|
|
185
|
+
empty={
|
|
186
|
+
state.level === 'healthy'
|
|
187
|
+
? 'Kyverno records nothing created for this request.'
|
|
188
|
+
: 'Nothing has been created yet.'
|
|
189
|
+
}
|
|
190
|
+
/>
|
|
191
|
+
</Section>
|
|
192
|
+
)}
|
|
193
|
+
</>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Same mapping the policy coverage view uses, so a result reads identically
|
|
198
|
+
// wherever it appears.
|
|
199
|
+
const RESULT_TONE: Record<string, string> = {
|
|
200
|
+
pass: SEVERITY_BADGE.success,
|
|
201
|
+
fail: SEVERITY_BADGE.error,
|
|
202
|
+
warn: SEVERITY_BADGE.warning,
|
|
203
|
+
error: SEVERITY_BADGE.alert,
|
|
204
|
+
skip: SEVERITY_BADGE.neutral,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* EphemeralReport — a background scan's findings for one resource, before they
|
|
209
|
+
* are folded into a PolicyReport.
|
|
210
|
+
*
|
|
211
|
+
* Worth a surface because it is what exists during a scan, and because a
|
|
212
|
+
* cluster mid-migration can show findings here that have not reached any
|
|
213
|
+
* PolicyReport yet. It is also genuinely short-lived: an empty list means the
|
|
214
|
+
* scan has finished, not that nothing was checked.
|
|
215
|
+
*/
|
|
216
|
+
export function KyvernoEphemeralReportRenderer({ data, onNavigate }: { data: any; onNavigate?: Nav }) {
|
|
217
|
+
const subject = getKyvernoReportSubject(data)
|
|
218
|
+
const summary = getKyvernoReportSummary(data)
|
|
219
|
+
const results = getKyvernoReportResults(data)
|
|
220
|
+
const source = getKyvernoReportSource(data)
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<>
|
|
224
|
+
<Section title="Scan" icon={FileWarning} defaultExpanded>
|
|
225
|
+
<PropertyList>
|
|
226
|
+
<Property
|
|
227
|
+
label="About"
|
|
228
|
+
value={
|
|
229
|
+
subject?.name ? (
|
|
230
|
+
<ResourceLink
|
|
231
|
+
name={subject.name}
|
|
232
|
+
kind={subject.kind ?? ''}
|
|
233
|
+
namespace={subject.namespace ?? ''}
|
|
234
|
+
group={subject.apiVersion?.includes('/') ? subject.apiVersion.split('/')[0] : undefined}
|
|
235
|
+
onNavigate={onNavigate}
|
|
236
|
+
/>
|
|
237
|
+
) : subject?.kind ? (
|
|
238
|
+
// The subject is gone; the labels still say what it was.
|
|
239
|
+
`${subject.kind} (deleted)`
|
|
240
|
+
) : (
|
|
241
|
+
'-'
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
/>
|
|
245
|
+
{source && <Property label="Produced by" value={source.replace(/-/g, ' ')} />}
|
|
246
|
+
<Property
|
|
247
|
+
label="Outcome"
|
|
248
|
+
value={
|
|
249
|
+
summary.total === 0
|
|
250
|
+
? 'Nothing was checked'
|
|
251
|
+
: [
|
|
252
|
+
summary.fail ? `${summary.fail} fail` : '',
|
|
253
|
+
summary.error ? `${summary.error} error` : '',
|
|
254
|
+
summary.warn ? `${summary.warn} warn` : '',
|
|
255
|
+
summary.pass ? `${summary.pass} pass` : '',
|
|
256
|
+
summary.skip ? `${summary.skip} skipped` : '',
|
|
257
|
+
]
|
|
258
|
+
.filter(Boolean)
|
|
259
|
+
.join(' · ')
|
|
260
|
+
}
|
|
261
|
+
/>
|
|
262
|
+
</PropertyList>
|
|
263
|
+
</Section>
|
|
264
|
+
|
|
265
|
+
<Section title={`Results (${results.length})`} icon={ListChecks} defaultExpanded>
|
|
266
|
+
{results.length === 0 ? (
|
|
267
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
268
|
+
This report carries no results. Kyverno deletes these once the scan is folded into a
|
|
269
|
+
PolicyReport, so an empty one is finished work rather than a problem.
|
|
270
|
+
</div>
|
|
271
|
+
) : (
|
|
272
|
+
<div className="space-y-1">
|
|
273
|
+
{results.map((r: any, i: number) => {
|
|
274
|
+
const at = kyvernoReportTimestamp(r?.timestamp)
|
|
275
|
+
return (
|
|
276
|
+
<div
|
|
277
|
+
key={`${r?.policy}/${r?.rule}/${i}`}
|
|
278
|
+
className="flex items-baseline gap-2 text-xs py-1 border-b border-theme-border/30 last:border-b-0"
|
|
279
|
+
>
|
|
280
|
+
<span
|
|
281
|
+
className={clsx('badge badge-sm shrink-0', RESULT_TONE[r?.result] || SEVERITY_BADGE.neutral)}
|
|
282
|
+
>
|
|
283
|
+
{r?.result || '-'}
|
|
284
|
+
</span>
|
|
285
|
+
<span className="text-theme-text-secondary truncate">{r?.policy || '-'}</span>
|
|
286
|
+
{r?.rule && <span className="text-theme-text-tertiary truncate">/ {r.rule}</span>}
|
|
287
|
+
{r?.message && (
|
|
288
|
+
<span className="text-theme-text-tertiary truncate" title={r.message}>
|
|
289
|
+
{r.message}
|
|
290
|
+
</span>
|
|
291
|
+
)}
|
|
292
|
+
{at && (
|
|
293
|
+
<span className="ml-auto shrink-0 text-theme-text-tertiary">{formatAge(at)} ago</span>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
})}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</Section>
|
|
301
|
+
</>
|
|
302
|
+
)
|
|
303
|
+
}
|