@skyhook-io/k8s-ui 1.3.2 → 1.4.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/audit/AuditAlerts.tsx +88 -0
- package/src/components/audit/AuditCard.tsx +130 -0
- package/src/components/audit/AuditFindingsTable.tsx +526 -0
- package/src/components/audit/index.ts +3 -0
- package/src/components/dock/DockContext.tsx +5 -2
- package/src/components/dock/LocalTerminalTab.tsx +11 -0
- package/src/components/resources/ResourcesView.tsx +14 -0
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +228 -0
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +174 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +10 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/shared/CreateResourceDialog.tsx +236 -0
- package/src/components/shared/EditableYamlView.tsx +16 -1
- package/src/components/shared/ResourceActionsBar.tsx +9 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +9 -2
- package/src/components/shared/index.ts +1 -0
- package/src/components/topology/K8sResourceNode.tsx +15 -0
- package/src/components/topology/TopologyControls.tsx +21 -1
- package/src/components/topology/TopologyGraph.tsx +1 -1
- package/src/components/ui/Badge.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +20 -4
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/types/core.ts +2 -1
- package/src/utils/badge-colors.ts +7 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/k8s-errors.ts +142 -0
- package/src/utils/resource-icons.ts +3 -0
- package/src/utils/skeleton-yaml.ts +302 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { Shield, ArrowDownToLine, ArrowUpFromLine, Ban } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
3
|
+
|
|
4
|
+
interface CiliumNetworkPolicyRendererProps {
|
|
5
|
+
data: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function CiliumNetworkPolicyRenderer({ data }: CiliumNetworkPolicyRendererProps) {
|
|
9
|
+
const spec = data.spec || data.specs || {}
|
|
10
|
+
const endpointSelector = spec.endpointSelector || {}
|
|
11
|
+
const matchLabels = endpointSelector.matchLabels || {}
|
|
12
|
+
const hasMatchLabels = Object.keys(matchLabels).length > 0
|
|
13
|
+
const ingress: any[] | undefined = spec.ingress
|
|
14
|
+
const ingressDeny: any[] | undefined = spec.ingressDeny
|
|
15
|
+
const egress: any[] | undefined = spec.egress
|
|
16
|
+
const egressDeny: any[] | undefined = spec.egressDeny
|
|
17
|
+
const description = spec.description || ''
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<Section title="Target" icon={Shield}>
|
|
22
|
+
{description && (
|
|
23
|
+
<p className="text-xs text-theme-text-secondary mb-2">{description}</p>
|
|
24
|
+
)}
|
|
25
|
+
<PropertyList>
|
|
26
|
+
<Property
|
|
27
|
+
label="Endpoint Selector"
|
|
28
|
+
value={hasMatchLabels ? undefined : 'All endpoints in namespace'}
|
|
29
|
+
/>
|
|
30
|
+
</PropertyList>
|
|
31
|
+
{hasMatchLabels && (
|
|
32
|
+
<div className="mt-2">
|
|
33
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Endpoint Selector</div>
|
|
34
|
+
<div className="flex flex-wrap gap-1">
|
|
35
|
+
{Object.entries(matchLabels).map(([k, v]) => (
|
|
36
|
+
<span key={k} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
37
|
+
{k}={String(v)}
|
|
38
|
+
</span>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
{spec.nodeSelector && (
|
|
44
|
+
<div className="mt-2">
|
|
45
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Node Selector</div>
|
|
46
|
+
<div className="flex flex-wrap gap-1">
|
|
47
|
+
{Object.entries(spec.nodeSelector.matchLabels || {}).map(([k, v]) => (
|
|
48
|
+
<span key={k} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
49
|
+
{k}={String(v)}
|
|
50
|
+
</span>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
</Section>
|
|
56
|
+
|
|
57
|
+
{ingress && ingress.length > 0 && (
|
|
58
|
+
<Section title="Ingress Allow" icon={ArrowDownToLine} defaultExpanded>
|
|
59
|
+
<div className="space-y-3">
|
|
60
|
+
{ingress.map((rule: any, i: number) => (
|
|
61
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</Section>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{ingressDeny && ingressDeny.length > 0 && (
|
|
68
|
+
<Section title="Ingress Deny" icon={Ban} defaultExpanded>
|
|
69
|
+
<div className="space-y-3">
|
|
70
|
+
{ingressDeny.map((rule: any, i: number) => (
|
|
71
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</Section>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{egress && egress.length > 0 && (
|
|
78
|
+
<Section title="Egress Allow" icon={ArrowUpFromLine} defaultExpanded>
|
|
79
|
+
<div className="space-y-3">
|
|
80
|
+
{egress.map((rule: any, i: number) => (
|
|
81
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</Section>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{egressDeny && egressDeny.length > 0 && (
|
|
88
|
+
<Section title="Egress Deny" icon={Ban} defaultExpanded>
|
|
89
|
+
<div className="space-y-3">
|
|
90
|
+
{egressDeny.map((rule: any, i: number) => (
|
|
91
|
+
<CiliumRuleCard key={i} rule={rule} />
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</Section>
|
|
95
|
+
)}
|
|
96
|
+
</>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function CiliumRuleCard({ rule }: { rule: any }) {
|
|
101
|
+
const fromEndpoints: any[] = rule.fromEndpoints || []
|
|
102
|
+
const fromEntities: string[] = rule.fromEntities || []
|
|
103
|
+
const fromCIDR: string[] = rule.fromCIDR || []
|
|
104
|
+
const fromCIDRSet: any[] = rule.fromCIDRSet || []
|
|
105
|
+
const toEndpoints: any[] = rule.toEndpoints || []
|
|
106
|
+
const toEntities: string[] = rule.toEntities || []
|
|
107
|
+
const toCIDR: string[] = rule.toCIDR || []
|
|
108
|
+
const toCIDRSet: any[] = rule.toCIDRSet || []
|
|
109
|
+
const toPorts: any[] = rule.toPorts || []
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="card-inner-lg">
|
|
113
|
+
{fromEndpoints.length > 0 && (
|
|
114
|
+
<div className="mb-2">
|
|
115
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Endpoints</div>
|
|
116
|
+
<div className="space-y-1">
|
|
117
|
+
{fromEndpoints.map((ep: any, j: number) => (
|
|
118
|
+
<SelectorEntry key={j} selector={ep} />
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
123
|
+
|
|
124
|
+
{fromEntities.length > 0 && (
|
|
125
|
+
<div className="mb-2">
|
|
126
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From Entities</div>
|
|
127
|
+
<div className="flex flex-wrap gap-1">
|
|
128
|
+
{fromEntities.map((e, j) => (
|
|
129
|
+
<span key={j} className="badge bg-purple-500/20 text-purple-400 border-purple-500/30">{e}</span>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{(fromCIDR.length > 0 || fromCIDRSet.length > 0) && (
|
|
136
|
+
<div className="mb-2">
|
|
137
|
+
<div className="text-xs text-theme-text-tertiary mb-1">From CIDR</div>
|
|
138
|
+
<div className="flex flex-wrap gap-1">
|
|
139
|
+
{fromCIDR.map((c, j) => (
|
|
140
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">{c}</span>
|
|
141
|
+
))}
|
|
142
|
+
{fromCIDRSet.map((cs: any, j: number) => (
|
|
143
|
+
<span key={`set-${j}`} className="badge bg-theme-elevated text-theme-text-secondary">{cs.cidr}</span>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{toEndpoints.length > 0 && (
|
|
150
|
+
<div className="mb-2">
|
|
151
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Endpoints</div>
|
|
152
|
+
<div className="space-y-1">
|
|
153
|
+
{toEndpoints.map((ep: any, j: number) => (
|
|
154
|
+
<SelectorEntry key={j} selector={ep} />
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{toEntities.length > 0 && (
|
|
161
|
+
<div className="mb-2">
|
|
162
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To Entities</div>
|
|
163
|
+
<div className="flex flex-wrap gap-1">
|
|
164
|
+
{toEntities.map((e, j) => (
|
|
165
|
+
<span key={j} className="badge bg-purple-500/20 text-purple-400 border-purple-500/30">{e}</span>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{(toCIDR.length > 0 || toCIDRSet.length > 0) && (
|
|
172
|
+
<div className="mb-2">
|
|
173
|
+
<div className="text-xs text-theme-text-tertiary mb-1">To CIDR</div>
|
|
174
|
+
<div className="flex flex-wrap gap-1">
|
|
175
|
+
{toCIDR.map((c, j) => (
|
|
176
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">{c}</span>
|
|
177
|
+
))}
|
|
178
|
+
{toCIDRSet.map((cs: any, j: number) => (
|
|
179
|
+
<span key={`set-${j}`} className="badge bg-theme-elevated text-theme-text-secondary">{cs.cidr}</span>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{toPorts.length > 0 && (
|
|
186
|
+
<div>
|
|
187
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Ports</div>
|
|
188
|
+
<div className="flex flex-wrap gap-1">
|
|
189
|
+
{toPorts.map((p: any, j: number) => {
|
|
190
|
+
const ports = p.ports || []
|
|
191
|
+
return ports.map((port: any, k: number) => (
|
|
192
|
+
<span key={`${j}-${k}`} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
193
|
+
{port.protocol || 'TCP'}/{port.port}
|
|
194
|
+
</span>
|
|
195
|
+
))
|
|
196
|
+
})}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{fromEndpoints.length === 0 && fromEntities.length === 0 && fromCIDR.length === 0 && fromCIDRSet.length === 0 &&
|
|
202
|
+
toEndpoints.length === 0 && toEntities.length === 0 && toCIDR.length === 0 && toCIDRSet.length === 0 &&
|
|
203
|
+
toPorts.length === 0 && (
|
|
204
|
+
<div className="text-xs text-theme-text-tertiary">Allow all</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function SelectorEntry({ selector }: { selector: any }) {
|
|
211
|
+
const matchLabels = selector.matchLabels || {}
|
|
212
|
+
const hasLabels = Object.keys(matchLabels).length > 0
|
|
213
|
+
return (
|
|
214
|
+
<div className="text-sm">
|
|
215
|
+
{hasLabels ? (
|
|
216
|
+
<span className="inline-flex flex-wrap gap-1 align-middle">
|
|
217
|
+
{Object.entries(matchLabels).map(([k, v]) => (
|
|
218
|
+
<span key={k} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
219
|
+
{k}={String(v)}
|
|
220
|
+
</span>
|
|
221
|
+
))}
|
|
222
|
+
</span>
|
|
223
|
+
) : (
|
|
224
|
+
<span className="text-xs text-theme-text-tertiary">all endpoints</span>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Shield, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
|
+
|
|
5
|
+
interface ClusterNetworkPolicyRendererProps {
|
|
6
|
+
data: any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ClusterNetworkPolicyRenderer({ data }: ClusterNetworkPolicyRendererProps) {
|
|
10
|
+
const spec = data.spec || {}
|
|
11
|
+
const subject = spec.subject || {}
|
|
12
|
+
const priority = spec.priority
|
|
13
|
+
const ingress: any[] | undefined = spec.ingress
|
|
14
|
+
const egress: any[] | undefined = spec.egress
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<Section title="Subject" icon={Shield}>
|
|
19
|
+
<PropertyList>
|
|
20
|
+
{priority !== undefined && (
|
|
21
|
+
<Property label="Priority" value={String(priority)} />
|
|
22
|
+
)}
|
|
23
|
+
</PropertyList>
|
|
24
|
+
{subject.namespaces && (
|
|
25
|
+
<div className="mt-2">
|
|
26
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Namespace Selector</div>
|
|
27
|
+
<SelectorBadges selector={subject.namespaces} />
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
{subject.pods && (
|
|
31
|
+
<div className="mt-2">
|
|
32
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
|
|
33
|
+
{subject.pods.namespaceSelector && (
|
|
34
|
+
<div className="mb-1">
|
|
35
|
+
<span className="text-xs text-theme-text-secondary">namespaces: </span>
|
|
36
|
+
<SelectorBadges selector={subject.pods.namespaceSelector} />
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
{subject.pods.podSelector && (
|
|
40
|
+
<div>
|
|
41
|
+
<span className="text-xs text-theme-text-secondary">pods: </span>
|
|
42
|
+
<SelectorBadges selector={subject.pods.podSelector} />
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</Section>
|
|
48
|
+
|
|
49
|
+
{ingress && ingress.length > 0 && (
|
|
50
|
+
<Section title={`Ingress Rules (${ingress.length})`} icon={ArrowDownToLine} defaultExpanded>
|
|
51
|
+
<div className="space-y-3">
|
|
52
|
+
{ingress.map((rule: any, i: number) => (
|
|
53
|
+
<AdminRuleCard key={i} rule={rule} direction="from" />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</Section>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{egress && egress.length > 0 && (
|
|
60
|
+
<Section title={`Egress Rules (${egress.length})`} icon={ArrowUpFromLine} defaultExpanded>
|
|
61
|
+
<div className="space-y-3">
|
|
62
|
+
{egress.map((rule: any, i: number) => (
|
|
63
|
+
<AdminRuleCard key={i} rule={rule} direction="to" />
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</Section>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function AdminRuleCard({ rule, direction }: { rule: any; direction: 'from' | 'to' }) {
|
|
73
|
+
const action = rule.action || 'Allow'
|
|
74
|
+
const name = rule.name || ''
|
|
75
|
+
const peers: any[] = rule[direction] || []
|
|
76
|
+
const ports: any[] = rule.ports || []
|
|
77
|
+
|
|
78
|
+
const actionColor = action === 'Deny'
|
|
79
|
+
? 'bg-red-500/20 text-red-400 border-red-500/30'
|
|
80
|
+
: action === 'Pass'
|
|
81
|
+
? 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'
|
|
82
|
+
: 'bg-green-500/20 text-green-400 border-green-500/30'
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="card-inner-lg">
|
|
86
|
+
<div className="flex items-center gap-2 mb-2">
|
|
87
|
+
<span className={clsx('badge', actionColor)}>{action}</span>
|
|
88
|
+
{name && <span className="text-xs text-theme-text-secondary">{name}</span>}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{peers.length > 0 && (
|
|
92
|
+
<div className="mb-2">
|
|
93
|
+
<div className="text-xs text-theme-text-tertiary mb-1 capitalize">{direction}</div>
|
|
94
|
+
<div className="space-y-1.5">
|
|
95
|
+
{peers.map((peer: any, j: number) => (
|
|
96
|
+
<AdminPeerEntry key={j} peer={peer} />
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{ports.length > 0 && (
|
|
103
|
+
<div>
|
|
104
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Ports</div>
|
|
105
|
+
<div className="flex flex-wrap gap-1">
|
|
106
|
+
{ports.map((port: any, j: number) => (
|
|
107
|
+
<span key={j} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
108
|
+
{port.portNumber?.protocol || 'TCP'}/{port.portNumber?.port || port.portRange?.start || '*'}
|
|
109
|
+
{port.portRange && `-${port.portRange.end}`}
|
|
110
|
+
</span>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function AdminPeerEntry({ peer }: { peer: any }) {
|
|
120
|
+
if (peer.namespaces) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="text-sm">
|
|
123
|
+
<span className="text-theme-text-secondary text-xs">namespaces: </span>
|
|
124
|
+
<SelectorBadges selector={peer.namespaces} />
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
if (peer.pods) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="text-sm space-y-0.5">
|
|
131
|
+
{peer.pods.namespaceSelector && (
|
|
132
|
+
<div>
|
|
133
|
+
<span className="text-theme-text-secondary text-xs">namespaces: </span>
|
|
134
|
+
<SelectorBadges selector={peer.pods.namespaceSelector} />
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
{peer.pods.podSelector && (
|
|
138
|
+
<div>
|
|
139
|
+
<span className="text-theme-text-secondary text-xs">pods: </span>
|
|
140
|
+
<SelectorBadges selector={peer.pods.podSelector} />
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
if (peer.networks) {
|
|
147
|
+
return (
|
|
148
|
+
<div className="text-sm">
|
|
149
|
+
<span className="text-theme-text-secondary text-xs">networks: </span>
|
|
150
|
+
<span className="inline-flex flex-wrap gap-1">
|
|
151
|
+
{peer.networks.map((n: string, i: number) => (
|
|
152
|
+
<span key={i} className="badge bg-theme-elevated text-theme-text-secondary">{n}</span>
|
|
153
|
+
))}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function SelectorBadges({ selector }: { selector: any }) {
|
|
162
|
+
const matchLabels = selector?.matchLabels || {}
|
|
163
|
+
const hasLabels = Object.keys(matchLabels).length > 0
|
|
164
|
+
if (!hasLabels) return <span className="text-xs text-theme-text-tertiary">all</span>
|
|
165
|
+
return (
|
|
166
|
+
<span className="inline-flex flex-wrap gap-1 align-middle">
|
|
167
|
+
{Object.entries(matchLabels).map(([k, v]) => (
|
|
168
|
+
<span key={k} className="badge bg-theme-elevated text-theme-text-secondary">
|
|
169
|
+
{k}={String(v)}
|
|
170
|
+
</span>
|
|
171
|
+
))}
|
|
172
|
+
</span>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
@@ -358,7 +358,7 @@ function NestedObjectViewer({ name, value, depth = 0 }: NestedObjectViewerProps)
|
|
|
358
358
|
<div className="ml-5 mt-1 space-y-1">
|
|
359
359
|
{simpleEntries.map(([k, v]) => (
|
|
360
360
|
<div key={k} className="flex items-start gap-2 text-xs">
|
|
361
|
-
<span className="text-theme-text-tertiary w-
|
|
361
|
+
<span className="text-theme-text-tertiary w-40 shrink-0">{formatFieldName(k)}</span>
|
|
362
362
|
<span className="text-theme-text-secondary break-all">{formatValue(v)}</span>
|
|
363
363
|
</div>
|
|
364
364
|
))}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { clsx } from 'clsx'
|
|
2
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
3
|
+
|
|
4
|
+
interface NetworkPolicyDiagramProps {
|
|
5
|
+
spec: any
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Visual flow diagram for a NetworkPolicy.
|
|
10
|
+
* Three-column layout: Sources -> Target -> Destinations
|
|
11
|
+
* Each rule is a horizontal band. Peers within a rule are OR'd (stacked).
|
|
12
|
+
* Pure CSS/SVG — no ReactFlow needed for this static diagram.
|
|
13
|
+
*/
|
|
14
|
+
export function NetworkPolicyDiagram({ spec }: NetworkPolicyDiagramProps) {
|
|
15
|
+
const podSelector = spec.podSelector || {}
|
|
16
|
+
const matchLabels = podSelector.matchLabels || {}
|
|
17
|
+
const policyTypes: string[] = spec.policyTypes || []
|
|
18
|
+
const ingress: any[] | undefined = spec.ingress
|
|
19
|
+
const egress: any[] | undefined = spec.egress
|
|
20
|
+
|
|
21
|
+
const hasIngress = policyTypes.includes('Ingress')
|
|
22
|
+
const hasEgress = policyTypes.includes('Egress')
|
|
23
|
+
const targetLabel = Object.keys(matchLabels).length > 0
|
|
24
|
+
? Object.entries(matchLabels).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
25
|
+
: 'All pods'
|
|
26
|
+
|
|
27
|
+
const ingressDenied = hasIngress && (!ingress || ingress.length === 0)
|
|
28
|
+
const egressDenied = hasEgress && (!egress || egress.length === 0)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="card-inner-lg space-y-3">
|
|
32
|
+
{/* Ingress flows */}
|
|
33
|
+
{hasIngress && (
|
|
34
|
+
<div className="space-y-2">
|
|
35
|
+
<div className="flex items-center gap-1.5">
|
|
36
|
+
<span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
|
|
37
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-blue-400">Ingress</span>
|
|
38
|
+
</div>
|
|
39
|
+
{ingressDenied ? (
|
|
40
|
+
<FlowRow
|
|
41
|
+
sources={[{ label: 'All traffic', type: 'deny' as const }]}
|
|
42
|
+
target={targetLabel}
|
|
43
|
+
ports={[]}
|
|
44
|
+
direction="ingress"
|
|
45
|
+
denied
|
|
46
|
+
/>
|
|
47
|
+
) : (
|
|
48
|
+
ingress?.map((rule, i) => (
|
|
49
|
+
<FlowRow
|
|
50
|
+
key={i}
|
|
51
|
+
sources={extractPeers(rule.from)}
|
|
52
|
+
target={targetLabel}
|
|
53
|
+
ports={extractPorts(rule.ports)}
|
|
54
|
+
direction="ingress"
|
|
55
|
+
/>
|
|
56
|
+
))
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{/* Egress flows */}
|
|
62
|
+
{hasEgress && (
|
|
63
|
+
<div className="space-y-2">
|
|
64
|
+
<div className="flex items-center gap-1.5">
|
|
65
|
+
<span className="w-1.5 h-1.5 rounded-full bg-purple-500" />
|
|
66
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-purple-400">Egress</span>
|
|
67
|
+
</div>
|
|
68
|
+
{egressDenied ? (
|
|
69
|
+
<FlowRow
|
|
70
|
+
sources={[{ label: 'All traffic', type: 'deny' as const }]}
|
|
71
|
+
target={targetLabel}
|
|
72
|
+
ports={[]}
|
|
73
|
+
direction="egress"
|
|
74
|
+
denied
|
|
75
|
+
/>
|
|
76
|
+
) : (
|
|
77
|
+
egress?.map((rule, i) => (
|
|
78
|
+
<FlowRow
|
|
79
|
+
key={i}
|
|
80
|
+
sources={extractPeers(rule.to)}
|
|
81
|
+
target={targetLabel}
|
|
82
|
+
ports={extractPorts(rule.ports)}
|
|
83
|
+
direction="egress"
|
|
84
|
+
/>
|
|
85
|
+
))
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{!hasIngress && !hasEgress && (
|
|
91
|
+
<div className="text-xs text-theme-text-tertiary text-center py-2">No policy types specified</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface PeerInfo {
|
|
98
|
+
label: string
|
|
99
|
+
sublabel?: string
|
|
100
|
+
type: 'pod' | 'namespace' | 'cidr' | 'all' | 'deny' | 'combined'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractPeers(peers: any[] | undefined): PeerInfo[] {
|
|
104
|
+
if (!peers || peers.length === 0) {
|
|
105
|
+
return [{ label: 'Any source', type: 'all' }]
|
|
106
|
+
}
|
|
107
|
+
return peers.map(peer => {
|
|
108
|
+
const hasPodSel = peer.podSelector !== undefined
|
|
109
|
+
const hasNsSel = peer.namespaceSelector !== undefined
|
|
110
|
+
const hasIpBlock = peer.ipBlock !== undefined
|
|
111
|
+
|
|
112
|
+
if (hasPodSel && hasNsSel) {
|
|
113
|
+
// AND: both must match
|
|
114
|
+
const podLabels = formatSelector(peer.podSelector)
|
|
115
|
+
const nsLabels = formatSelector(peer.namespaceSelector)
|
|
116
|
+
return {
|
|
117
|
+
label: podLabels,
|
|
118
|
+
sublabel: `in ${nsLabels === 'all' ? 'any namespace' : nsLabels}`,
|
|
119
|
+
type: 'combined' as const,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (hasPodSel) {
|
|
123
|
+
return { label: formatSelector(peer.podSelector), type: 'pod' as const }
|
|
124
|
+
}
|
|
125
|
+
if (hasNsSel) {
|
|
126
|
+
const nsLabels = formatSelector(peer.namespaceSelector)
|
|
127
|
+
return { label: nsLabels === 'all' ? 'All namespaces' : nsLabels, type: 'namespace' as const }
|
|
128
|
+
}
|
|
129
|
+
if (hasIpBlock) {
|
|
130
|
+
const cidr = peer.ipBlock.cidr || '?'
|
|
131
|
+
const except = peer.ipBlock.except
|
|
132
|
+
return {
|
|
133
|
+
label: cidr,
|
|
134
|
+
sublabel: except?.length ? `except ${except.join(', ')}` : undefined,
|
|
135
|
+
type: 'cidr' as const,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { label: '?', type: 'all' as const }
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractPorts(ports: any[] | undefined): string[] {
|
|
143
|
+
if (!ports || ports.length === 0) return []
|
|
144
|
+
return ports.map((p: any) => {
|
|
145
|
+
const proto = p.protocol || 'TCP'
|
|
146
|
+
const port = p.port || '*'
|
|
147
|
+
const endPort = p.endPort
|
|
148
|
+
return endPort ? `${proto}/${port}-${endPort}` : `${proto}/${port}`
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatSelector(selector: any): string {
|
|
153
|
+
const labels = selector?.matchLabels || {}
|
|
154
|
+
if (Object.keys(labels).length === 0) return 'all'
|
|
155
|
+
return Object.entries(labels).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Color coding for peer types
|
|
159
|
+
const PEER_STYLES = {
|
|
160
|
+
pod: 'border-emerald-500/30 bg-emerald-500/8',
|
|
161
|
+
namespace: 'border-sky-500/30 bg-sky-500/8',
|
|
162
|
+
cidr: 'border-amber-500/30 bg-amber-500/8',
|
|
163
|
+
combined: 'border-emerald-500/30 bg-emerald-500/8',
|
|
164
|
+
all: 'border-theme-border bg-theme-elevated/50',
|
|
165
|
+
deny: 'border-red-500/30 bg-red-500/8',
|
|
166
|
+
} as const
|
|
167
|
+
|
|
168
|
+
const PEER_DOT = {
|
|
169
|
+
pod: 'bg-emerald-500',
|
|
170
|
+
namespace: 'bg-sky-500',
|
|
171
|
+
cidr: 'bg-amber-500',
|
|
172
|
+
combined: 'bg-emerald-500',
|
|
173
|
+
all: 'bg-theme-text-tertiary',
|
|
174
|
+
deny: 'bg-red-500',
|
|
175
|
+
} as const
|
|
176
|
+
|
|
177
|
+
function FlowRow({
|
|
178
|
+
sources,
|
|
179
|
+
target,
|
|
180
|
+
ports,
|
|
181
|
+
direction,
|
|
182
|
+
denied = false,
|
|
183
|
+
}: {
|
|
184
|
+
sources: PeerInfo[]
|
|
185
|
+
target: string
|
|
186
|
+
ports: string[]
|
|
187
|
+
direction: 'ingress' | 'egress'
|
|
188
|
+
denied?: boolean
|
|
189
|
+
}) {
|
|
190
|
+
const isIngress = direction === 'ingress'
|
|
191
|
+
|
|
192
|
+
const sourceNodes = (
|
|
193
|
+
<div className="flex-1 min-w-0 space-y-1">
|
|
194
|
+
{sources.map((peer, i) => (
|
|
195
|
+
<div key={i}>
|
|
196
|
+
{i > 0 && (
|
|
197
|
+
<div className="text-[9px] text-theme-text-tertiary uppercase tracking-widest text-center my-0.5">or</div>
|
|
198
|
+
)}
|
|
199
|
+
<Tooltip content={peer.sublabel ? `${peer.label}\n${peer.sublabel}` : peer.label} position="top">
|
|
200
|
+
<div
|
|
201
|
+
className={clsx(
|
|
202
|
+
'rounded-md border px-2 py-1.5',
|
|
203
|
+
PEER_STYLES[peer.type],
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
<div className="flex items-center gap-1.5">
|
|
207
|
+
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', PEER_DOT[peer.type])} />
|
|
208
|
+
<span className={clsx('text-[11px] font-medium truncate', denied && 'line-through text-red-400')}>
|
|
209
|
+
{peer.label}
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
{peer.sublabel && (
|
|
213
|
+
<div className="text-[10px] text-theme-text-tertiary ml-3 truncate">{peer.sublabel}</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</Tooltip>
|
|
217
|
+
</div>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const targetNode = (
|
|
223
|
+
<Tooltip content={target} position="top">
|
|
224
|
+
<div className="rounded-md border border-indigo-500/30 bg-indigo-500/8 px-2 py-1.5 flex-1 min-w-0">
|
|
225
|
+
<div className="flex items-center gap-1.5">
|
|
226
|
+
<span className="w-1.5 h-1.5 rounded-full shrink-0 bg-indigo-500" />
|
|
227
|
+
<span className="text-[11px] font-medium truncate">{target}</span>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</Tooltip>
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const arrow = (
|
|
234
|
+
<div className="flex flex-col items-center justify-center shrink-0 w-10">
|
|
235
|
+
{/* Arrow line with optional port label */}
|
|
236
|
+
<svg width="40" height="20" viewBox="0 0 40 20" className="overflow-visible">
|
|
237
|
+
<defs>
|
|
238
|
+
<marker id={`arrow-${direction}-${denied ? 'denied' : 'ok'}`} markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">
|
|
239
|
+
<path d="M0,0 L6,3 L0,6" fill="none" stroke={denied ? '#ef4444' : direction === 'ingress' ? '#3b82f6' : '#a855f7'} strokeWidth="1.5" />
|
|
240
|
+
</marker>
|
|
241
|
+
</defs>
|
|
242
|
+
<line
|
|
243
|
+
x1="2" y1="10" x2="32" y2="10"
|
|
244
|
+
stroke={denied ? '#ef4444' : direction === 'ingress' ? '#3b82f6' : '#a855f7'}
|
|
245
|
+
strokeWidth="1.5"
|
|
246
|
+
strokeDasharray={denied ? '3 2' : undefined}
|
|
247
|
+
markerEnd={`url(#arrow-${direction}-${denied ? 'denied' : 'ok'})`}
|
|
248
|
+
/>
|
|
249
|
+
</svg>
|
|
250
|
+
{ports.length > 0 && (
|
|
251
|
+
<div className="flex flex-wrap justify-center gap-0.5 mt-0.5">
|
|
252
|
+
{ports.map((p, i) => (
|
|
253
|
+
<span key={i} className="text-[8px] text-theme-text-tertiary font-mono leading-none">{p}</span>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div className="flex items-center gap-1">
|
|
262
|
+
{isIngress ? (
|
|
263
|
+
<>
|
|
264
|
+
{sourceNodes}
|
|
265
|
+
{arrow}
|
|
266
|
+
{targetNode}
|
|
267
|
+
</>
|
|
268
|
+
) : (
|
|
269
|
+
<>
|
|
270
|
+
{targetNode}
|
|
271
|
+
{arrow}
|
|
272
|
+
{sourceNodes}
|
|
273
|
+
</>
|
|
274
|
+
)}
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|