@skyhook-io/k8s-ui 1.3.2 → 1.4.1
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 +2 -2
- 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 +126 -49
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +190 -28
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +22 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +156 -5
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +28 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +47 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +22 -3
- package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +445 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +81 -9
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +10 -3
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +59 -1
- package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +160 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -5
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +91 -3
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +3 -41
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +66 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +31 -1
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +66 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +55 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +23 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +38 -3
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +277 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +17 -57
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +3 -42
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +3 -8
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/PodRenderer.tsx +22 -74
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +178 -20
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +2 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +105 -3
- package/src/components/resources/renderers/SbomReportRenderer.tsx +46 -5
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +11 -2
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +5 -7
- package/src/components/resources/renderers/ServiceRenderer.tsx +40 -28
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +11 -3
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +11 -3
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +8 -16
- package/src/components/resources/renderers/WorkflowRenderer.tsx +71 -17
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +46 -1
- package/src/components/resources/renderers/index.ts +3 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +18 -3
- package/src/components/resources/resource-utils-cnpg.ts +47 -0
- package/src/components/resources/resource-utils-karpenter.ts +6 -0
- package/src/components/resources/resource-utils-prometheus.ts +50 -6
- package/src/components/resources/resource-utils.ts +131 -33
- 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 +13 -6
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +11 -4
- 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 +68 -5
- package/src/components/workload/WorkloadView.tsx +55 -29
- package/src/index.ts +3 -0
- package/src/theme/variables.css +1 -1
- package/src/types/core.ts +2 -1
- package/src/types/gitops.ts +8 -0
- 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/navigation.test.ts +29 -2
- package/src/utils/navigation.ts +45 -24
- package/src/utils/resource-icons.ts +3 -0
- package/src/utils/skeleton-yaml.ts +302 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Server, Settings, Shield, Cpu, Tag } from 'lucide-react'
|
|
1
|
+
import { Server, Settings, Shield, Cpu, Tag, BarChart3 } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
3
|
import { kindToPlural } from '../../../utils/navigation'
|
|
4
4
|
import {
|
|
@@ -9,6 +9,16 @@ import {
|
|
|
9
9
|
getNodePoolWeight,
|
|
10
10
|
} from '../resource-utils-karpenter'
|
|
11
11
|
|
|
12
|
+
function formatCpuCores(value: string): string {
|
|
13
|
+
// Karpenter status.resources CPU is typically in millicores (e.g. "12000m") or cores (e.g. "12")
|
|
14
|
+
if (value.endsWith('m')) {
|
|
15
|
+
const millis = parseInt(value, 10)
|
|
16
|
+
if (!isNaN(millis)) return String(millis / 1000)
|
|
17
|
+
}
|
|
18
|
+
return value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
12
22
|
interface KarpenterNodePoolRendererProps {
|
|
13
23
|
data: any
|
|
14
24
|
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
@@ -28,6 +38,9 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
|
|
|
28
38
|
const templateLabels = spec.template?.metadata?.labels || {}
|
|
29
39
|
const templateExpireAfter = spec.template?.spec?.expireAfter
|
|
30
40
|
const nodeClassRef = spec.template?.spec?.nodeClassRef
|
|
41
|
+
const templateTaints = spec.template?.spec?.taints || []
|
|
42
|
+
const templateStartupTaints = spec.template?.spec?.startupTaints || []
|
|
43
|
+
const statusResources = status.resources || {}
|
|
31
44
|
|
|
32
45
|
return (
|
|
33
46
|
<>
|
|
@@ -77,6 +90,26 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
|
|
|
77
90
|
</PropertyList>
|
|
78
91
|
</Section>
|
|
79
92
|
|
|
93
|
+
{/* Resource Usage — from status.resources vs spec.limits */}
|
|
94
|
+
{(statusResources.cpu || statusResources.memory) && (
|
|
95
|
+
<Section title="Resource Usage" icon={BarChart3} defaultExpanded>
|
|
96
|
+
<PropertyList>
|
|
97
|
+
{statusResources.cpu && (
|
|
98
|
+
<Property
|
|
99
|
+
label="CPU"
|
|
100
|
+
value={`${formatCpuCores(statusResources.cpu)}${spec.limits?.cpu ? ` / ${formatCpuCores(spec.limits.cpu)}` : ''}`}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
{statusResources.memory && (
|
|
104
|
+
<Property
|
|
105
|
+
label="Memory"
|
|
106
|
+
value={`${statusResources.memory}${spec.limits?.memory ? ` / ${spec.limits.memory}` : ''}`}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
</PropertyList>
|
|
110
|
+
</Section>
|
|
111
|
+
)}
|
|
112
|
+
|
|
80
113
|
{/* Disruption */}
|
|
81
114
|
<Section title="Disruption" icon={Shield}>
|
|
82
115
|
<PropertyList>
|
|
@@ -118,6 +151,38 @@ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoo
|
|
|
118
151
|
</Section>
|
|
119
152
|
)}
|
|
120
153
|
|
|
154
|
+
{/* Template Taints */}
|
|
155
|
+
{templateTaints.length > 0 && (
|
|
156
|
+
<Section title={`Template Taints (${templateTaints.length})`} icon={Shield} defaultExpanded>
|
|
157
|
+
<div className="flex flex-wrap gap-1">
|
|
158
|
+
{templateTaints.map((taint: any, i: number) => (
|
|
159
|
+
<span
|
|
160
|
+
key={i}
|
|
161
|
+
className="badge-sm bg-theme-hover text-theme-text-secondary"
|
|
162
|
+
>
|
|
163
|
+
{taint.key}={taint.value || ''}:{taint.effect || ''}
|
|
164
|
+
</span>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
</Section>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* Template Startup Taints */}
|
|
171
|
+
{templateStartupTaints.length > 0 && (
|
|
172
|
+
<Section title={`Startup Taints (${templateStartupTaints.length})`} icon={Shield} defaultExpanded>
|
|
173
|
+
<div className="flex flex-wrap gap-1">
|
|
174
|
+
{templateStartupTaints.map((taint: any, i: number) => (
|
|
175
|
+
<span
|
|
176
|
+
key={i}
|
|
177
|
+
className="badge-sm bg-theme-hover text-theme-text-secondary"
|
|
178
|
+
>
|
|
179
|
+
{taint.key}={taint.value || ''}:{taint.effect || ''}
|
|
180
|
+
</span>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
</Section>
|
|
184
|
+
)}
|
|
185
|
+
|
|
121
186
|
{/* Requirements */}
|
|
122
187
|
{requirements.length > 0 && (
|
|
123
188
|
<Section title={`Requirements (${requirements.length})`} icon={Settings} defaultExpanded>
|
|
@@ -46,6 +46,9 @@ export function KedaScaledJobRenderer({ data }: KedaScaledJobRendererProps) {
|
|
|
46
46
|
{spec.failedJobsHistoryLimit !== undefined && (
|
|
47
47
|
<Property label="Failure History" value={String(spec.failedJobsHistoryLimit)} />
|
|
48
48
|
)}
|
|
49
|
+
{spec.minReplicaCount !== undefined && (
|
|
50
|
+
<Property label="Min Replicas" value={String(spec.minReplicaCount)} />
|
|
51
|
+
)}
|
|
49
52
|
{spec.maxReplicaCount !== undefined && (
|
|
50
53
|
<Property label="Max Replicas" value={String(spec.maxReplicaCount)} />
|
|
51
54
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cpu } from 'lucide-react'
|
|
1
|
+
import { Cpu, Settings } from 'lucide-react'
|
|
2
2
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
3
3
|
import { kindToPlural } from '../../../utils/navigation'
|
|
4
4
|
import {
|
|
@@ -12,6 +12,16 @@ import {
|
|
|
12
12
|
getScaledObjectCooldownPeriod,
|
|
13
13
|
} from '../resource-utils-keda'
|
|
14
14
|
|
|
15
|
+
function summarizeScalingPolicies(policies: any[] | undefined, periodSeconds?: number): string {
|
|
16
|
+
if (!policies || policies.length === 0) return '-'
|
|
17
|
+
return policies.map((p: any) => {
|
|
18
|
+
const value = p.value ?? '?'
|
|
19
|
+
const type = p.type === 'Percent' ? '%' : ` pod${value !== 1 ? 's' : ''}`
|
|
20
|
+
const period = p.periodSeconds ?? periodSeconds ?? '?'
|
|
21
|
+
return `max ${value}${type}/${period}s`
|
|
22
|
+
}).join(', ')
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
interface KedaScaledObjectRendererProps {
|
|
16
26
|
data: any
|
|
17
27
|
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
@@ -131,6 +141,50 @@ export function KedaScaledObjectRenderer({ data, onNavigate }: KedaScaledObjectR
|
|
|
131
141
|
</Section>
|
|
132
142
|
)}
|
|
133
143
|
|
|
144
|
+
{/* Advanced section */}
|
|
145
|
+
{data.spec?.advanced && (
|
|
146
|
+
<Section title="Advanced" icon={Settings} defaultExpanded={false}>
|
|
147
|
+
<PropertyList>
|
|
148
|
+
{data.spec.advanced.restoreToOriginalReplicaCount != null && (
|
|
149
|
+
<Property
|
|
150
|
+
label="Restore Original Replicas"
|
|
151
|
+
value={data.spec.advanced.restoreToOriginalReplicaCount ? 'Yes' : 'No'}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
{data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleUp && (
|
|
155
|
+
<Property
|
|
156
|
+
label="Scale Up"
|
|
157
|
+
value={summarizeScalingPolicies(
|
|
158
|
+
data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.policies,
|
|
159
|
+
data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.stabilizationWindowSeconds
|
|
160
|
+
)}
|
|
161
|
+
/>
|
|
162
|
+
)}
|
|
163
|
+
{data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleDown && (
|
|
164
|
+
<Property
|
|
165
|
+
label="Scale Down"
|
|
166
|
+
value={summarizeScalingPolicies(
|
|
167
|
+
data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.policies,
|
|
168
|
+
data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds
|
|
169
|
+
)}
|
|
170
|
+
/>
|
|
171
|
+
)}
|
|
172
|
+
{data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleUp?.stabilizationWindowSeconds != null && (
|
|
173
|
+
<Property
|
|
174
|
+
label="Scale Up Stabilization"
|
|
175
|
+
value={`${data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleUp.stabilizationWindowSeconds}s`}
|
|
176
|
+
/>
|
|
177
|
+
)}
|
|
178
|
+
{data.spec.advanced.horizontalPodAutoscalerConfig?.behavior?.scaleDown?.stabilizationWindowSeconds != null && (
|
|
179
|
+
<Property
|
|
180
|
+
label="Scale Down Stabilization"
|
|
181
|
+
value={`${data.spec.advanced.horizontalPodAutoscalerConfig.behavior.scaleDown.stabilizationWindowSeconds}s`}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
</PropertyList>
|
|
185
|
+
</Section>
|
|
186
|
+
)}
|
|
187
|
+
|
|
134
188
|
<ConditionsSection conditions={conditions} />
|
|
135
189
|
</>
|
|
136
190
|
)
|
|
@@ -19,6 +19,7 @@ export function KedaTriggerAuthRenderer({ data, onNavigate }: KedaTriggerAuthRen
|
|
|
19
19
|
const vault = spec.hashiCorpVault
|
|
20
20
|
const azureKeyVault = spec.azureKeyVault
|
|
21
21
|
const awsSecretManager = spec.awsSecretManager
|
|
22
|
+
const gcpSecretManager = spec.gcpSecretManager
|
|
22
23
|
const podIdentity = spec.podIdentity
|
|
23
24
|
|
|
24
25
|
return (
|
|
@@ -145,6 +146,28 @@ export function KedaTriggerAuthRenderer({ data, onNavigate }: KedaTriggerAuthRen
|
|
|
145
146
|
</Section>
|
|
146
147
|
)}
|
|
147
148
|
|
|
149
|
+
{/* GCP Secret Manager */}
|
|
150
|
+
{gcpSecretManager && (
|
|
151
|
+
<Section title="GCP Secret Manager" icon={Cloud}>
|
|
152
|
+
<PropertyList>
|
|
153
|
+
{gcpSecretManager.credentials?.clientSecret?.name && (
|
|
154
|
+
<Property label="Credentials Secret" value={gcpSecretManager.credentials.clientSecret.name} />
|
|
155
|
+
)}
|
|
156
|
+
</PropertyList>
|
|
157
|
+
{gcpSecretManager.secrets && gcpSecretManager.secrets.length > 0 && (
|
|
158
|
+
<div className="mt-2 space-y-1">
|
|
159
|
+
<div className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-1">Secrets</div>
|
|
160
|
+
{gcpSecretManager.secrets.map((s: any, i: number) => (
|
|
161
|
+
<div key={i} className="card-inner text-sm">
|
|
162
|
+
<span className="text-theme-text-primary font-medium">{s.parameter}</span>
|
|
163
|
+
<span className="text-theme-text-secondary ml-2">{s.id}{s.version ? ` (v${s.version})` : ''}</span>
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</Section>
|
|
169
|
+
)}
|
|
170
|
+
|
|
148
171
|
{conditions.length > 0 && <ConditionsSection conditions={conditions} />}
|
|
149
172
|
</>
|
|
150
173
|
)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { Layers, FolderTree } from 'lucide-react'
|
|
2
|
-
import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts } from '../../ui/drawer-components'
|
|
1
|
+
import { Layers, FolderTree, GitBranch } from 'lucide-react'
|
|
2
|
+
import { Section, PropertyList, Property, ConditionsSection, ProblemAlerts, ResourceLink } from '../../ui/drawer-components'
|
|
3
3
|
import { formatAge } from '../resource-utils'
|
|
4
4
|
import { GitOpsStatusBadge, ManagedResourcesList, SyncCountdown } from '../../gitops'
|
|
5
5
|
import { fluxConditionsToGitOpsStatus, parseFluxInventory, type FluxCondition } from '../../../types/gitops'
|
|
6
6
|
|
|
7
7
|
interface KustomizationRendererProps {
|
|
8
8
|
data: any
|
|
9
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export function KustomizationRenderer({ data }: KustomizationRendererProps) {
|
|
12
|
+
export function KustomizationRenderer({ data, onNavigate }: KustomizationRendererProps) {
|
|
12
13
|
const status = data.status || {}
|
|
13
14
|
const spec = data.spec || {}
|
|
14
15
|
const conditions = (status.conditions || []) as FluxCondition[]
|
|
@@ -39,6 +40,23 @@ export function KustomizationRenderer({ data }: KustomizationRendererProps) {
|
|
|
39
40
|
})
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// Revision mismatch detection — only flag when Ready=False to avoid false positives during active reconciliation
|
|
44
|
+
const isReadyFalse = conditions.find((c: any) => c.type === 'Ready')?.status === 'False'
|
|
45
|
+
if (
|
|
46
|
+
isReadyFalse &&
|
|
47
|
+
status.lastAppliedRevision &&
|
|
48
|
+
status.lastAttemptedRevision &&
|
|
49
|
+
status.lastAppliedRevision !== status.lastAttemptedRevision
|
|
50
|
+
) {
|
|
51
|
+
problems.push({
|
|
52
|
+
color: 'red',
|
|
53
|
+
message: `Revision mismatch: attempted ${status.lastAttemptedRevision} but applied ${status.lastAppliedRevision} — the latest reconciliation failed.`,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Dependencies
|
|
58
|
+
const dependsOn: Array<{ name: string; namespace?: string }> = spec.dependsOn || []
|
|
59
|
+
|
|
42
60
|
// Source reference
|
|
43
61
|
const sourceRef = spec.sourceRef || {}
|
|
44
62
|
|
|
@@ -94,6 +112,23 @@ export function KustomizationRenderer({ data }: KustomizationRendererProps) {
|
|
|
94
112
|
</PropertyList>
|
|
95
113
|
</Section>
|
|
96
114
|
|
|
115
|
+
{/* Dependencies */}
|
|
116
|
+
{dependsOn.length > 0 && (
|
|
117
|
+
<Section title={`Dependencies (${dependsOn.length})`} icon={GitBranch}>
|
|
118
|
+
<div className="flex flex-wrap gap-1">
|
|
119
|
+
{dependsOn.map((dep, idx) => (
|
|
120
|
+
<ResourceLink
|
|
121
|
+
key={idx}
|
|
122
|
+
name={dep.name}
|
|
123
|
+
kind="kustomizations"
|
|
124
|
+
namespace={dep.namespace || data.metadata?.namespace || ''}
|
|
125
|
+
onNavigate={onNavigate}
|
|
126
|
+
/>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</Section>
|
|
130
|
+
)}
|
|
131
|
+
|
|
97
132
|
{/* Health checks */}
|
|
98
133
|
{spec.healthChecks && spec.healthChecks.length > 0 && (
|
|
99
134
|
<Section title={`Health Checks (${spec.healthChecks.length})`} defaultExpanded={false}>
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Shield, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react'
|
|
1
|
+
import { Shield, ArrowDownToLine, ArrowUpFromLine, GitFork } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section,
|
|
3
|
+
import { Section, LabelSelectorDisplay } from '../../ui/drawer-components'
|
|
4
|
+
import { NetworkPolicyDiagram } from './NetworkPolicyDiagram'
|
|
4
5
|
|
|
5
6
|
interface NetworkPolicyRendererProps {
|
|
6
7
|
data: any
|
|
@@ -13,35 +14,24 @@ export function NetworkPolicyRenderer({ data }: NetworkPolicyRendererProps) {
|
|
|
13
14
|
const ingress: any[] | undefined = spec.ingress
|
|
14
15
|
const egress: any[] | undefined = spec.egress
|
|
15
16
|
|
|
16
|
-
const matchLabels = podSelector.matchLabels || {}
|
|
17
|
-
const hasMatchLabels = Object.keys(matchLabels).length > 0
|
|
18
17
|
const hasIngress = policyTypes.includes('Ingress')
|
|
19
18
|
const hasEgress = policyTypes.includes('Egress')
|
|
20
19
|
|
|
20
|
+
const hasDiagramContent = hasIngress || hasEgress
|
|
21
|
+
|
|
21
22
|
return (
|
|
22
23
|
<>
|
|
24
|
+
{hasDiagramContent && (
|
|
25
|
+
<Section title="Policy Flow" icon={GitFork} defaultExpanded>
|
|
26
|
+
<NetworkPolicyDiagram spec={spec} />
|
|
27
|
+
</Section>
|
|
28
|
+
)}
|
|
29
|
+
|
|
23
30
|
<Section title="Target" icon={Shield}>
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/>
|
|
29
|
-
</PropertyList>
|
|
30
|
-
{hasMatchLabels && (
|
|
31
|
-
<div className="mt-2">
|
|
32
|
-
<div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
|
|
33
|
-
<div className="flex flex-wrap gap-1">
|
|
34
|
-
{Object.entries(matchLabels).map(([k, v]) => (
|
|
35
|
-
<span
|
|
36
|
-
key={k}
|
|
37
|
-
className="badge bg-theme-elevated text-theme-text-secondary"
|
|
38
|
-
>
|
|
39
|
-
{k}={String(v)}
|
|
40
|
-
</span>
|
|
41
|
-
))}
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
)}
|
|
31
|
+
<div className="mt-2">
|
|
32
|
+
<div className="text-xs text-theme-text-tertiary mb-1">Pod Selector</div>
|
|
33
|
+
<LabelSelectorDisplay selector={podSelector} emptyText="All pods in namespace" />
|
|
34
|
+
</div>
|
|
45
35
|
{policyTypes.length > 0 && (
|
|
46
36
|
<div className="mt-2">
|
|
47
37
|
<div className="text-xs text-theme-text-tertiary mb-1">Policy Types</div>
|
|
@@ -143,49 +133,19 @@ function IngressEgressRuleCard({
|
|
|
143
133
|
|
|
144
134
|
function PeerEntry({ peer }: { peer: any }) {
|
|
145
135
|
if (peer.podSelector) {
|
|
146
|
-
const labels = peer.podSelector.matchLabels || {}
|
|
147
|
-
const hasLabels = Object.keys(labels).length > 0
|
|
148
136
|
return (
|
|
149
137
|
<div className="text-sm">
|
|
150
138
|
<span className="text-theme-text-secondary text-xs">podSelector: </span>
|
|
151
|
-
{
|
|
152
|
-
<span className="inline-flex flex-wrap gap-1 align-middle">
|
|
153
|
-
{Object.entries(labels).map(([k, v]) => (
|
|
154
|
-
<span
|
|
155
|
-
key={k}
|
|
156
|
-
className="badge bg-theme-elevated text-theme-text-secondary"
|
|
157
|
-
>
|
|
158
|
-
{k}={String(v)}
|
|
159
|
-
</span>
|
|
160
|
-
))}
|
|
161
|
-
</span>
|
|
162
|
-
) : (
|
|
163
|
-
<span className="text-xs text-theme-text-tertiary">all pods</span>
|
|
164
|
-
)}
|
|
139
|
+
<LabelSelectorDisplay selector={peer.podSelector} emptyText="all pods" inline />
|
|
165
140
|
</div>
|
|
166
141
|
)
|
|
167
142
|
}
|
|
168
143
|
|
|
169
144
|
if (peer.namespaceSelector) {
|
|
170
|
-
const labels = peer.namespaceSelector.matchLabels || {}
|
|
171
|
-
const hasLabels = Object.keys(labels).length > 0
|
|
172
145
|
return (
|
|
173
146
|
<div className="text-sm">
|
|
174
147
|
<span className="text-theme-text-secondary text-xs">namespaceSelector: </span>
|
|
175
|
-
{
|
|
176
|
-
<span className="inline-flex flex-wrap gap-1 align-middle">
|
|
177
|
-
{Object.entries(labels).map(([k, v]) => (
|
|
178
|
-
<span
|
|
179
|
-
key={k}
|
|
180
|
-
className="badge bg-theme-elevated text-theme-text-secondary"
|
|
181
|
-
>
|
|
182
|
-
{k}={String(v)}
|
|
183
|
-
</span>
|
|
184
|
-
))}
|
|
185
|
-
</span>
|
|
186
|
-
) : (
|
|
187
|
-
<span className="text-xs text-theme-text-tertiary">all namespaces</span>
|
|
188
|
-
)}
|
|
148
|
+
<LabelSelectorDisplay selector={peer.namespaceSelector} emptyText="all namespaces" inline />
|
|
189
149
|
</div>
|
|
190
150
|
)
|
|
191
151
|
}
|