@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
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowDownToLine,
|
|
3
|
+
ArrowUpFromLine,
|
|
4
|
+
GitFork,
|
|
5
|
+
ShieldCheck,
|
|
6
|
+
} from 'lucide-react'
|
|
7
|
+
import { Badge, type BadgeSeverity } from '../../ui/Badge'
|
|
8
|
+
import {
|
|
9
|
+
Property,
|
|
10
|
+
PropertyList,
|
|
11
|
+
ResourceRefBadge,
|
|
12
|
+
Section,
|
|
13
|
+
} from '../../ui/drawer-components'
|
|
14
|
+
import type { ResourceRef } from '../../../types'
|
|
15
|
+
import {
|
|
16
|
+
getCalicoTierRef,
|
|
17
|
+
getCalicoPolicyKindLabel,
|
|
18
|
+
getCalicoPolicySelector,
|
|
19
|
+
getCalicoPolicyTypes,
|
|
20
|
+
formatKubernetesLabelSelector,
|
|
21
|
+
isCalicoStagedDeletion,
|
|
22
|
+
isCalicoStagedIgnored,
|
|
23
|
+
} from '../resource-utils-calico'
|
|
24
|
+
import { CalicoNetworkPolicyDiagram } from './CalicoNetworkPolicyDiagram'
|
|
25
|
+
|
|
26
|
+
interface CalicoNetworkPolicyRendererProps {
|
|
27
|
+
data: any
|
|
28
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ACTION_SEVERITY: Record<string, BadgeSeverity> = {
|
|
32
|
+
allow: 'success',
|
|
33
|
+
deny: 'error',
|
|
34
|
+
log: 'warning',
|
|
35
|
+
pass: 'info',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function CalicoNetworkPolicyRenderer({
|
|
39
|
+
data,
|
|
40
|
+
onNavigate,
|
|
41
|
+
}: CalicoNetworkPolicyRendererProps) {
|
|
42
|
+
const spec = data?.spec ?? {}
|
|
43
|
+
const kindLabel = getCalicoPolicyKindLabel(data?.kind)
|
|
44
|
+
const staged = kindLabel.includes('Staged')
|
|
45
|
+
const ingress = Array.isArray(spec.ingress) ? spec.ingress : undefined
|
|
46
|
+
const egress = Array.isArray(spec.egress) ? spec.egress : undefined
|
|
47
|
+
const types = getCalicoPolicyTypes(data)
|
|
48
|
+
const tierRef = getCalicoTierRef(data)
|
|
49
|
+
// A staged deletion has no selector and no rules to draw — it stages the
|
|
50
|
+
// removal of the enforced policy with the same name. An ignored staged policy
|
|
51
|
+
// has rules, but Calico skips it, so they are not a preview of anything.
|
|
52
|
+
const stagedDeletion = isCalicoStagedDeletion(data)
|
|
53
|
+
const stagedIgnored = isCalicoStagedIgnored(data)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
<Section title="Policy Flow" icon={GitFork} defaultExpanded>
|
|
58
|
+
{stagedDeletion || stagedIgnored ? (
|
|
59
|
+
<div className="card-inner-lg flex flex-col gap-1.5">
|
|
60
|
+
<div className="flex items-center gap-2 text-[11px] text-theme-text-tertiary">
|
|
61
|
+
<Badge severity="warning" size="sm">
|
|
62
|
+
{stagedDeletion ? 'Staged deletion' : 'Staged, ignored'}
|
|
63
|
+
</Badge>
|
|
64
|
+
<span>Nothing is previewed as protected</span>
|
|
65
|
+
</div>
|
|
66
|
+
<span className="text-xs text-theme-text-secondary">
|
|
67
|
+
{stagedDeletion ? (
|
|
68
|
+
<>
|
|
69
|
+
Promoting this removes {data?.metadata?.name ?? 'the policy'}{' '}
|
|
70
|
+
from the {String(spec.tier ?? 'default')} tier. Any workload
|
|
71
|
+
protected only by that policy loses its coverage.
|
|
72
|
+
</>
|
|
73
|
+
) : (
|
|
74
|
+
<>
|
|
75
|
+
Calico skips a staged policy whose action is Ignore, so
|
|
76
|
+
promoting the staged set changes nothing here.
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<CalicoNetworkPolicyDiagram spec={spec} staged={staged} />
|
|
83
|
+
)}
|
|
84
|
+
</Section>
|
|
85
|
+
|
|
86
|
+
<Section title="Policy" icon={ShieldCheck}>
|
|
87
|
+
<div className="space-y-3">
|
|
88
|
+
<Badge kind={kindLabel}>{kindLabel}</Badge>
|
|
89
|
+
<PropertyList>
|
|
90
|
+
<Property
|
|
91
|
+
label="Selector"
|
|
92
|
+
value={
|
|
93
|
+
<SelectorValue
|
|
94
|
+
value={stagedDeletion ? '' : getCalicoPolicySelector(data)}
|
|
95
|
+
emptyText={
|
|
96
|
+
stagedDeletion ? 'not applicable' : 'all workloads'
|
|
97
|
+
}
|
|
98
|
+
/>
|
|
99
|
+
}
|
|
100
|
+
/>
|
|
101
|
+
{spec.namespaceSelector !== undefined && (
|
|
102
|
+
<Property
|
|
103
|
+
label="Namespace Selector"
|
|
104
|
+
value={
|
|
105
|
+
<SelectorValue
|
|
106
|
+
value={spec.namespaceSelector}
|
|
107
|
+
emptyText="all namespaces"
|
|
108
|
+
/>
|
|
109
|
+
}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
{spec.serviceAccountSelector !== undefined && (
|
|
113
|
+
<Property
|
|
114
|
+
label="Service Account Selector"
|
|
115
|
+
value={
|
|
116
|
+
<SelectorValue
|
|
117
|
+
value={spec.serviceAccountSelector}
|
|
118
|
+
emptyText="all service accounts"
|
|
119
|
+
/>
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
<Property
|
|
124
|
+
label="Tier"
|
|
125
|
+
value={
|
|
126
|
+
<ResourceRefBadge resourceRef={tierRef} onClick={onNavigate} />
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
{spec.order !== undefined && (
|
|
130
|
+
<Property label="Order" value={spec.order} />
|
|
131
|
+
)}
|
|
132
|
+
{types.length > 0 && (
|
|
133
|
+
<Property label="Types" value={<BadgeList values={types} />} />
|
|
134
|
+
)}
|
|
135
|
+
{staged && spec.stagedAction !== undefined && (
|
|
136
|
+
<Property
|
|
137
|
+
label="Staged Action"
|
|
138
|
+
value={<Badge tone="note">{String(spec.stagedAction)}</Badge>}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
{spec.preDNAT !== undefined && (
|
|
142
|
+
<Property label="Pre-DNAT" value={yesNo(spec.preDNAT)} />
|
|
143
|
+
)}
|
|
144
|
+
{spec.applyOnForward !== undefined && (
|
|
145
|
+
<Property
|
|
146
|
+
label="Apply On Forward"
|
|
147
|
+
value={yesNo(spec.applyOnForward)}
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
{spec.doNotTrack !== undefined && (
|
|
151
|
+
<Property label="Do Not Track" value={yesNo(spec.doNotTrack)} />
|
|
152
|
+
)}
|
|
153
|
+
{Array.isArray(spec.performanceHints) &&
|
|
154
|
+
spec.performanceHints.length > 0 && (
|
|
155
|
+
<Property
|
|
156
|
+
label="Performance Hints"
|
|
157
|
+
value={
|
|
158
|
+
<BadgeList values={spec.performanceHints} tone="note" />
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
</PropertyList>
|
|
163
|
+
</div>
|
|
164
|
+
</Section>
|
|
165
|
+
|
|
166
|
+
{ingress && (
|
|
167
|
+
<RuleSection
|
|
168
|
+
title={`Ingress Rules (${ingress.length})`}
|
|
169
|
+
icon={ArrowDownToLine}
|
|
170
|
+
rules={ingress}
|
|
171
|
+
direction="ingress"
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
{egress && (
|
|
175
|
+
<RuleSection
|
|
176
|
+
title={`Egress Rules (${egress.length})`}
|
|
177
|
+
icon={ArrowUpFromLine}
|
|
178
|
+
rules={egress}
|
|
179
|
+
direction="egress"
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
182
|
+
</>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function RuleSection({
|
|
187
|
+
title,
|
|
188
|
+
icon: Icon,
|
|
189
|
+
rules,
|
|
190
|
+
direction,
|
|
191
|
+
}: {
|
|
192
|
+
title: string
|
|
193
|
+
icon: React.ComponentType<{ className?: string }>
|
|
194
|
+
rules: any[]
|
|
195
|
+
direction: 'ingress' | 'egress'
|
|
196
|
+
}) {
|
|
197
|
+
return (
|
|
198
|
+
<Section title={title} icon={Icon} defaultExpanded>
|
|
199
|
+
{rules.length === 0 ? (
|
|
200
|
+
<div className="text-sm text-theme-text-tertiary">No rules</div>
|
|
201
|
+
) : (
|
|
202
|
+
<div className="space-y-3">
|
|
203
|
+
{rules.map((rule, index) => (
|
|
204
|
+
<CalicoRuleCard key={index} rule={rule} direction={direction} />
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</Section>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function CalicoRuleCard({
|
|
213
|
+
rule,
|
|
214
|
+
direction,
|
|
215
|
+
}: {
|
|
216
|
+
rule: any
|
|
217
|
+
direction: 'ingress' | 'egress'
|
|
218
|
+
}) {
|
|
219
|
+
const action = String(rule?.action ?? 'Allow')
|
|
220
|
+
const protocol = rule?.protocol
|
|
221
|
+
const notProtocol = rule?.notProtocol
|
|
222
|
+
const hasPorts = hasCalicoPortConstraints(rule)
|
|
223
|
+
const primaryEntity =
|
|
224
|
+
direction === 'ingress' ? rule?.source : rule?.destination
|
|
225
|
+
const secondaryEntity =
|
|
226
|
+
direction === 'ingress' ? rule?.destination : rule?.source
|
|
227
|
+
const primaryLabel = direction === 'ingress' ? 'Source' : 'Destination'
|
|
228
|
+
const secondaryLabel = direction === 'ingress' ? 'Destination' : 'Source'
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="card-inner-lg space-y-3">
|
|
232
|
+
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
233
|
+
<Badge severity={ACTION_SEVERITY[action.toLowerCase()] ?? 'neutral'}>
|
|
234
|
+
{action}
|
|
235
|
+
</Badge>
|
|
236
|
+
{protocol !== undefined && !hasPorts && (
|
|
237
|
+
<Badge protocol={formatProtocol(protocol)} size="sm">
|
|
238
|
+
{formatProtocol(protocol)}
|
|
239
|
+
</Badge>
|
|
240
|
+
)}
|
|
241
|
+
{notProtocol !== undefined && (
|
|
242
|
+
<Badge tone="note" size="sm">
|
|
243
|
+
not {formatProtocol(notProtocol)}
|
|
244
|
+
</Badge>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<EntityRuleBlock label={primaryLabel} entity={primaryEntity} />
|
|
249
|
+
<EntityRuleBlock label={secondaryLabel} entity={secondaryEntity} />
|
|
250
|
+
<PortBlock
|
|
251
|
+
label="Source Ports"
|
|
252
|
+
entity={rule?.source}
|
|
253
|
+
ruleProtocol={protocol}
|
|
254
|
+
/>
|
|
255
|
+
<PortBlock
|
|
256
|
+
label="Ports"
|
|
257
|
+
entity={rule?.destination}
|
|
258
|
+
ruleProtocol={protocol}
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
{(rule?.http !== undefined ||
|
|
262
|
+
rule?.icmp !== undefined ||
|
|
263
|
+
rule?.notICMP !== undefined) && (
|
|
264
|
+
<div className="space-y-2">
|
|
265
|
+
{rule.http !== undefined && <HTTPMatch value={rule.http} />}
|
|
266
|
+
{rule.icmp !== undefined && (
|
|
267
|
+
<ICMPMatch label="ICMP" value={rule.icmp} />
|
|
268
|
+
)}
|
|
269
|
+
{rule.notICMP !== undefined && (
|
|
270
|
+
<ICMPMatch label="Not ICMP" value={rule.notICMP} negative />
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface EntityRuleField {
|
|
279
|
+
label: string
|
|
280
|
+
value: React.ReactNode
|
|
281
|
+
negative?: boolean
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function EntityRuleBlock({ label, entity }: { label: string; entity: any }) {
|
|
285
|
+
const peers = (Array.isArray(entity) ? entity : [entity])
|
|
286
|
+
.map(entityFields)
|
|
287
|
+
.filter((fields) => fields.length > 0)
|
|
288
|
+
if (peers.length === 0) return null
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<div className="mb-2">
|
|
292
|
+
<div className="text-xs text-theme-text-tertiary mb-1">{label}</div>
|
|
293
|
+
<div className="space-y-1.5">
|
|
294
|
+
{peers.map((fields, peerIndex) => (
|
|
295
|
+
<div key={peerIndex} className="space-y-0.5 text-sm">
|
|
296
|
+
{fields.map((field, fieldIndex) => (
|
|
297
|
+
<div
|
|
298
|
+
key={`${field.label}-${fieldIndex}`}
|
|
299
|
+
className="flex flex-wrap items-baseline gap-1"
|
|
300
|
+
>
|
|
301
|
+
<span
|
|
302
|
+
className={
|
|
303
|
+
field.negative
|
|
304
|
+
? 'text-warning-text text-xs shrink-0'
|
|
305
|
+
: 'text-theme-text-secondary text-xs shrink-0'
|
|
306
|
+
}
|
|
307
|
+
>
|
|
308
|
+
{field.label}:
|
|
309
|
+
</span>
|
|
310
|
+
<div className="flex flex-wrap gap-1 min-w-0">
|
|
311
|
+
{field.value}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
))}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function entityFields(entity: any): EntityRuleField[] {
|
|
323
|
+
if (!entity || typeof entity !== 'object') return []
|
|
324
|
+
|
|
325
|
+
const fields: EntityRuleField[] = []
|
|
326
|
+
addEntityField(
|
|
327
|
+
fields,
|
|
328
|
+
'Pod Selector',
|
|
329
|
+
entity.podSelector === undefined
|
|
330
|
+
? undefined
|
|
331
|
+
: formatKubernetesLabelSelector(entity.podSelector),
|
|
332
|
+
)
|
|
333
|
+
addEntityField(fields, 'Selector', entity.selector)
|
|
334
|
+
addEntityField(fields, 'Not Selector', entity.notSelector, true)
|
|
335
|
+
addEntityField(
|
|
336
|
+
fields,
|
|
337
|
+
'Namespace Selector',
|
|
338
|
+
entity.namespaceSelector && typeof entity.namespaceSelector === 'object'
|
|
339
|
+
? formatKubernetesLabelSelector(entity.namespaceSelector)
|
|
340
|
+
: entity.namespaceSelector,
|
|
341
|
+
)
|
|
342
|
+
addEntityField(fields, 'IP Block', entity.ipBlock)
|
|
343
|
+
addEntityField(
|
|
344
|
+
fields,
|
|
345
|
+
'Not Namespace Selector',
|
|
346
|
+
entity.notNamespaceSelector,
|
|
347
|
+
true,
|
|
348
|
+
)
|
|
349
|
+
addEntityField(fields, 'Nets', entity.nets)
|
|
350
|
+
addEntityField(fields, 'Not Nets', entity.notNets, true)
|
|
351
|
+
addEntityField(fields, 'Service Accounts', entity.serviceAccounts)
|
|
352
|
+
addEntityField(
|
|
353
|
+
fields,
|
|
354
|
+
'Not Service Accounts',
|
|
355
|
+
entity.notServiceAccounts,
|
|
356
|
+
true,
|
|
357
|
+
)
|
|
358
|
+
return fields
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function addEntityField(
|
|
362
|
+
fields: EntityRuleField[],
|
|
363
|
+
label: string,
|
|
364
|
+
value: any,
|
|
365
|
+
negative = false,
|
|
366
|
+
) {
|
|
367
|
+
if (
|
|
368
|
+
value === undefined ||
|
|
369
|
+
value === null ||
|
|
370
|
+
value === '' ||
|
|
371
|
+
(Array.isArray(value) && value.length === 0)
|
|
372
|
+
)
|
|
373
|
+
return
|
|
374
|
+
fields.push({ label, value: renderEntityValue(value, negative), negative })
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function PortBlock({
|
|
378
|
+
label,
|
|
379
|
+
entity,
|
|
380
|
+
ruleProtocol,
|
|
381
|
+
}: {
|
|
382
|
+
label: string
|
|
383
|
+
entity: any
|
|
384
|
+
ruleProtocol: any
|
|
385
|
+
}) {
|
|
386
|
+
const entries = [
|
|
387
|
+
...valuesFor(entity?.ports).map((port) => ({
|
|
388
|
+
value: formatPort(port, ruleProtocol),
|
|
389
|
+
negative: false,
|
|
390
|
+
})),
|
|
391
|
+
...valuesFor(entity?.notPorts).map((port) => ({
|
|
392
|
+
value: formatPort(port, ruleProtocol),
|
|
393
|
+
negative: true,
|
|
394
|
+
})),
|
|
395
|
+
]
|
|
396
|
+
if (entries.length === 0) return null
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<div className="mb-2">
|
|
400
|
+
<div className="text-xs text-theme-text-tertiary mb-1">{label}</div>
|
|
401
|
+
<div className="flex flex-wrap gap-1">
|
|
402
|
+
{entries.map((entry, index) => (
|
|
403
|
+
<Badge
|
|
404
|
+
key={`${entry.value}-${index}`}
|
|
405
|
+
tone={entry.negative ? 'note' : 'structural'}
|
|
406
|
+
size="sm"
|
|
407
|
+
className={
|
|
408
|
+
entry.negative
|
|
409
|
+
? 'font-mono whitespace-nowrap line-through'
|
|
410
|
+
: 'font-mono whitespace-nowrap'
|
|
411
|
+
}
|
|
412
|
+
>
|
|
413
|
+
{entry.negative ? `not ${entry.value}` : entry.value}
|
|
414
|
+
</Badge>
|
|
415
|
+
))}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function hasCalicoPortConstraints(rule: any): boolean {
|
|
422
|
+
return (
|
|
423
|
+
valuesFor(rule?.source?.ports).length > 0 ||
|
|
424
|
+
valuesFor(rule?.source?.notPorts).length > 0 ||
|
|
425
|
+
valuesFor(rule?.destination?.ports).length > 0 ||
|
|
426
|
+
valuesFor(rule?.destination?.notPorts).length > 0
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function valuesFor(value: any): any[] {
|
|
431
|
+
if (value === undefined || value === null || value === '') return []
|
|
432
|
+
return Array.isArray(value) ? value : [value]
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function formatPort(value: any, ruleProtocol?: any): string {
|
|
436
|
+
if (value && typeof value === 'object') {
|
|
437
|
+
const protocol = value.protocol
|
|
438
|
+
? formatProtocol(value.protocol).toUpperCase()
|
|
439
|
+
: ruleProtocol !== undefined
|
|
440
|
+
? formatProtocol(ruleProtocol).toUpperCase()
|
|
441
|
+
: 'TCP'
|
|
442
|
+
if ('port' in value) {
|
|
443
|
+
const endPort = value.endPort !== undefined ? `-${value.endPort}` : ''
|
|
444
|
+
return `${protocol}/${value.port}${endPort}`
|
|
445
|
+
}
|
|
446
|
+
if ('start' in value || 'end' in value) {
|
|
447
|
+
return `${protocol}/${value.start ?? ''}-${value.end ?? ''}`
|
|
448
|
+
}
|
|
449
|
+
if ('strVal' in value) return `${protocol}/${value.strVal}`
|
|
450
|
+
if ('intVal' in value) return `${protocol}/${value.intVal}`
|
|
451
|
+
}
|
|
452
|
+
const protocol =
|
|
453
|
+
ruleProtocol !== undefined
|
|
454
|
+
? formatProtocol(ruleProtocol).toUpperCase()
|
|
455
|
+
: 'TCP'
|
|
456
|
+
return `${protocol}/${formatValue(value)}`
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function renderEntityValue(value: any, negative = false): React.ReactNode {
|
|
460
|
+
if (
|
|
461
|
+
value &&
|
|
462
|
+
typeof value === 'object' &&
|
|
463
|
+
!Array.isArray(value) &&
|
|
464
|
+
('selector' in value || 'namespaces' in value || 'names' in value)
|
|
465
|
+
) {
|
|
466
|
+
const items: React.ReactNode[] = []
|
|
467
|
+
if (value.selector !== undefined)
|
|
468
|
+
items.push(
|
|
469
|
+
<SelectorValue
|
|
470
|
+
key="selector"
|
|
471
|
+
value={value.selector}
|
|
472
|
+
negative={negative}
|
|
473
|
+
/>,
|
|
474
|
+
)
|
|
475
|
+
for (const name of value.names ?? value.namespaces ?? []) {
|
|
476
|
+
items.push(
|
|
477
|
+
<ValueBadge key={String(name)} value={name} negative={negative} />,
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
return items.length > 0 ? (
|
|
481
|
+
items
|
|
482
|
+
) : (
|
|
483
|
+
<ValueBadge value={JSON.stringify(value)} negative={negative} />
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const values = Array.isArray(value) ? value : [value]
|
|
488
|
+
return values.map((item, index) => (
|
|
489
|
+
<ValueBadge
|
|
490
|
+
key={`${String(item)}-${index}`}
|
|
491
|
+
value={formatValue(item)}
|
|
492
|
+
negative={negative}
|
|
493
|
+
/>
|
|
494
|
+
))
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function HTTPMatch({ value }: { value: any }) {
|
|
498
|
+
return (
|
|
499
|
+
<div className="card-inner text-xs space-y-2">
|
|
500
|
+
<div className="text-theme-text-tertiary">HTTP</div>
|
|
501
|
+
{value?.methods?.length > 0 && (
|
|
502
|
+
<InlineField label="Methods" values={value.methods} tone="accent1" />
|
|
503
|
+
)}
|
|
504
|
+
{value?.notMethods?.length > 0 && (
|
|
505
|
+
<InlineField
|
|
506
|
+
label="Not Methods"
|
|
507
|
+
values={value.notMethods}
|
|
508
|
+
tone="note"
|
|
509
|
+
/>
|
|
510
|
+
)}
|
|
511
|
+
{value?.paths?.length > 0 && (
|
|
512
|
+
<InlineField label="Paths" values={value.paths} />
|
|
513
|
+
)}
|
|
514
|
+
{value?.notPaths?.length > 0 && (
|
|
515
|
+
<InlineField label="Not Paths" values={value.notPaths} tone="note" />
|
|
516
|
+
)}
|
|
517
|
+
{!value?.methods?.length &&
|
|
518
|
+
!value?.notMethods?.length &&
|
|
519
|
+
!value?.paths?.length &&
|
|
520
|
+
!value?.notPaths?.length && (
|
|
521
|
+
<span className="text-theme-text-secondary">Any HTTP request</span>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function ICMPMatch({
|
|
528
|
+
label,
|
|
529
|
+
value,
|
|
530
|
+
negative = false,
|
|
531
|
+
}: {
|
|
532
|
+
label: string
|
|
533
|
+
value: any
|
|
534
|
+
negative?: boolean
|
|
535
|
+
}) {
|
|
536
|
+
const fields = Array.isArray(value) ? value : [value]
|
|
537
|
+
return (
|
|
538
|
+
<div className="card-inner text-xs">
|
|
539
|
+
<div className="text-theme-text-tertiary mb-1">{label}</div>
|
|
540
|
+
<div className="flex flex-wrap gap-1">
|
|
541
|
+
{fields.map((field, index) => {
|
|
542
|
+
const text =
|
|
543
|
+
field && typeof field === 'object'
|
|
544
|
+
? [
|
|
545
|
+
field.family !== undefined && `family ${field.family}`,
|
|
546
|
+
field.type !== undefined && `type ${field.type}`,
|
|
547
|
+
field.code !== undefined && `code ${field.code}`,
|
|
548
|
+
]
|
|
549
|
+
.filter(Boolean)
|
|
550
|
+
.join(', ') || JSON.stringify(field)
|
|
551
|
+
: String(field)
|
|
552
|
+
return (
|
|
553
|
+
<ValueBadge
|
|
554
|
+
key={`${text}-${index}`}
|
|
555
|
+
value={text || 'any'}
|
|
556
|
+
negative={negative}
|
|
557
|
+
/>
|
|
558
|
+
)
|
|
559
|
+
})}
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function InlineField({
|
|
566
|
+
label,
|
|
567
|
+
values,
|
|
568
|
+
tone,
|
|
569
|
+
}: {
|
|
570
|
+
label: string
|
|
571
|
+
values: any[]
|
|
572
|
+
tone?: 'note' | 'accent1' | 'structural'
|
|
573
|
+
}) {
|
|
574
|
+
return (
|
|
575
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
576
|
+
<span className="text-theme-text-secondary mr-1">{label}:</span>
|
|
577
|
+
{values.map((value, index) => (
|
|
578
|
+
<ValueBadge
|
|
579
|
+
key={`${String(value)}-${index}`}
|
|
580
|
+
value={value}
|
|
581
|
+
tone={tone}
|
|
582
|
+
/>
|
|
583
|
+
))}
|
|
584
|
+
</div>
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function SelectorValue({
|
|
589
|
+
value,
|
|
590
|
+
emptyText,
|
|
591
|
+
negative = false,
|
|
592
|
+
}: {
|
|
593
|
+
value: any
|
|
594
|
+
emptyText?: string
|
|
595
|
+
negative?: boolean
|
|
596
|
+
}) {
|
|
597
|
+
if (value === undefined || value === null || value === '') {
|
|
598
|
+
return (
|
|
599
|
+
<span className="text-theme-text-tertiary">{emptyText ?? 'Any'}</span>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
return <ValueBadge value={value} negative={negative} />
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function ValueBadge({
|
|
606
|
+
value,
|
|
607
|
+
negative = false,
|
|
608
|
+
tone,
|
|
609
|
+
}: {
|
|
610
|
+
value: any
|
|
611
|
+
negative?: boolean
|
|
612
|
+
tone?: 'note' | 'accent1' | 'structural'
|
|
613
|
+
}) {
|
|
614
|
+
return (
|
|
615
|
+
<Badge
|
|
616
|
+
tone={tone ?? (negative ? 'note' : 'structural')}
|
|
617
|
+
size="sm"
|
|
618
|
+
className="font-mono whitespace-normal break-all"
|
|
619
|
+
>
|
|
620
|
+
{String(value)}
|
|
621
|
+
</Badge>
|
|
622
|
+
)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function BadgeList({
|
|
626
|
+
values,
|
|
627
|
+
tone = 'structural',
|
|
628
|
+
}: {
|
|
629
|
+
values: any[]
|
|
630
|
+
tone?: 'note' | 'accent1' | 'structural'
|
|
631
|
+
}) {
|
|
632
|
+
return (
|
|
633
|
+
<span className="flex flex-wrap gap-1">
|
|
634
|
+
{values.map((value, index) => (
|
|
635
|
+
<Badge key={`${String(value)}-${index}`} tone={tone} size="sm">
|
|
636
|
+
{String(value)}
|
|
637
|
+
</Badge>
|
|
638
|
+
))}
|
|
639
|
+
</span>
|
|
640
|
+
)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function formatValue(value: any): string {
|
|
644
|
+
if (value && typeof value === 'object') {
|
|
645
|
+
if ('port' in value) {
|
|
646
|
+
const protocol = value.protocol ? `${value.protocol}/` : ''
|
|
647
|
+
const end = value.endPort !== undefined ? `-${value.endPort}` : ''
|
|
648
|
+
return `${protocol}${value.port}${end}`
|
|
649
|
+
}
|
|
650
|
+
if ('start' in value || 'end' in value)
|
|
651
|
+
return `${value.start ?? ''}-${value.end ?? ''}`
|
|
652
|
+
if ('strVal' in value) return String(value.strVal)
|
|
653
|
+
if ('intVal' in value) return String(value.intVal)
|
|
654
|
+
return JSON.stringify(value)
|
|
655
|
+
}
|
|
656
|
+
return String(value)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function yesNo(value: any): string {
|
|
660
|
+
return value ? 'Yes' : 'No'
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function formatProtocol(value: any): string {
|
|
664
|
+
if (value && typeof value === 'object') {
|
|
665
|
+
if (value.strVal !== undefined) return String(value.strVal)
|
|
666
|
+
if (value.intVal !== undefined) return String(value.intVal)
|
|
667
|
+
return JSON.stringify(value)
|
|
668
|
+
}
|
|
669
|
+
return String(value)
|
|
670
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Network } from 'lucide-react'
|
|
2
|
+
import { Property, PropertyList, Section } from '../../ui/drawer-components'
|
|
3
|
+
|
|
4
|
+
interface CalicoTierRendererProps {
|
|
5
|
+
data: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function CalicoTierRenderer({ data }: CalicoTierRendererProps) {
|
|
9
|
+
const spec = data?.spec ?? {}
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Section title="Tier" icon={Network}>
|
|
13
|
+
<PropertyList>
|
|
14
|
+
<Property label="Default Action" value={spec.defaultAction ?? 'Deny'} />
|
|
15
|
+
<Property label="Order" value={spec.order ?? 'Last (lowest precedence)'} />
|
|
16
|
+
</PropertyList>
|
|
17
|
+
</Section>
|
|
18
|
+
)
|
|
19
|
+
}
|