Skip to content

Commit 959fc62

Browse files
committed
frontend: network: Add support for Admin Network Policy resources
ANP - Alpha support - This change adds CRUD support for ANP. - Added an ANP component to the UI - Added a Kube Object to headlamp lib to represent ANP
1 parent fbd7a09 commit 959fc62

File tree

5 files changed

+654
-0
lines changed

5 files changed

+654
-0
lines changed

frontend/src/components/Sidebar/useSidebarItems.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,10 @@ export const useSidebarItems = (sidebarName: string = DefaultSidebars.IN_CLUSTER
268268
name: 'NetworkPolicies',
269269
label: t('glossary|Network Policies'),
270270
},
271+
{
272+
name: 'AdminNetworkPolicies',
273+
label: t('glossary|Admin Network Policies (alpha)'),
274+
},
271275
],
272276
},
273277
{
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Box from '@mui/material/Box';
18+
import Tooltip from '@mui/material/Tooltip';
19+
import Typography from '@mui/material/Typography';
20+
import { useTranslation } from 'react-i18next';
21+
import { useParams } from 'react-router';
22+
import { matchLabelsSimplifier } from '../../lib/k8s';
23+
import AdminNetworkPolicy, {
24+
AdminNetworkPolicyEgressRule,
25+
AdminNetworkPolicyIngressRule,
26+
AdminNetworkPolicyPort,
27+
} from '../../lib/k8s/adminnetworkpolicy';
28+
import NameValueTable from '../common/NameValueTable';
29+
import { DetailsGrid, MetadataDictGrid } from '../common/Resource';
30+
import { metadataStyles } from '../common/Resource';
31+
import SectionBox from '../common/SectionBox';
32+
import { KubeIcon } from '../resourceMap/kubeIcon/KubeIcon';
33+
34+
export default function AdminNetworkPolicyDetails(props: {
35+
name?: string;
36+
cluster?: string;
37+
subject?: string;
38+
}) {
39+
const params = useParams<{ subject: string; name: string }>();
40+
const { name = params.name, subject = params.subject, cluster } = props;
41+
const { t } = useTranslation(['glossary', 'translation']);
42+
43+
function SubjectSelector(props: { AdminNetworkPolicy: AdminNetworkPolicy }) {
44+
const { AdminNetworkPolicy } = props;
45+
// this is an exception which should never happen, but we handle it gracefully
46+
if (
47+
AdminNetworkPolicy.jsonData?.spec?.subject?.pods === undefined &&
48+
AdminNetworkPolicy.jsonData?.spec?.subject?.namespaces === undefined
49+
) {
50+
return <></>;
51+
}
52+
53+
if (AdminNetworkPolicy.jsonData?.spec?.subject?.pods !== undefined) {
54+
return (
55+
<>
56+
<KubeIcon kind="Namespace" width="30px" height="30px" />
57+
<Tooltip title="namespaceSelector">
58+
{(() => {
59+
const labels = matchLabelsSimplifier(
60+
AdminNetworkPolicy.jsonData?.spec?.subject?.pods?.namespaceSelector?.matchLabels
61+
);
62+
return Array.isArray(labels) ? (
63+
<>
64+
{labels.map((label: string) => (
65+
<Typography sx={metadataStyles} display="inline" key={label}>
66+
{label}
67+
</Typography>
68+
))}
69+
</>
70+
) : (
71+
<Typography sx={metadataStyles} display="inline">
72+
All Namespaces
73+
</Typography>
74+
);
75+
})()}
76+
</Tooltip>
77+
<KubeIcon kind="Pod" width="30px" height="30px" />
78+
<Tooltip title="podSelector">
79+
{(() => {
80+
const labels = matchLabelsSimplifier(
81+
AdminNetworkPolicy.jsonData?.spec?.subject?.pods?.podSelector?.matchLabels
82+
);
83+
return Array.isArray(labels) ? (
84+
<>
85+
{labels.map((label: string) => (
86+
<Typography sx={metadataStyles} display="inline" key={label}>
87+
{label}
88+
</Typography>
89+
))}
90+
</>
91+
) : (
92+
<Typography sx={metadataStyles} display="inline">
93+
All Pods
94+
</Typography>
95+
);
96+
})()}
97+
</Tooltip>
98+
</>
99+
);
100+
}
101+
102+
if (AdminNetworkPolicy.jsonData?.spec?.subject?.namespaces !== undefined) {
103+
return (
104+
<>
105+
<Typography sx={metadataStyles} display="inline">
106+
{(() => {
107+
const labels = matchLabelsSimplifier(
108+
AdminNetworkPolicy.jsonData?.spec?.subject?.namespaces?.namespaceSelector
109+
?.matchLabels
110+
);
111+
return Array.isArray(labels) ? (
112+
<>
113+
{labels.map((label: string) => (
114+
<Typography sx={metadataStyles} display="inline" key={label}>
115+
{label}
116+
</Typography>
117+
))}
118+
</>
119+
) : (
120+
<Typography sx={metadataStyles} display="inline">
121+
All Pods
122+
</Typography>
123+
);
124+
})()}
125+
</Typography>
126+
</>
127+
);
128+
}
129+
}
130+
131+
function Ingress(props: { ingress: AdminNetworkPolicyIngressRule[] }) {
132+
const { ingress } = props;
133+
134+
if (!ingress || ingress.length === 0) {
135+
return <></>;
136+
}
137+
138+
return (
139+
<>
140+
{ingress.map((item: AdminNetworkPolicyIngressRule, index: number) => (
141+
<SectionBox key={`ingress-${index}`} title={t('Ingress')}>
142+
<NameValueTable
143+
rows={[
144+
{
145+
name: t('Name'),
146+
value: item.name || <Box sx={metadataStyles}>-</Box>,
147+
},
148+
{
149+
name: t('translation|Action'),
150+
value: (
151+
<>
152+
<span color="success">{item.action}</span>
153+
</>
154+
),
155+
},
156+
{
157+
name: t('Ports'),
158+
value: item.ports?.map((ports: AdminNetworkPolicyPort) => {
159+
if (!ports.portNumber) {
160+
return <></>;
161+
}
162+
if (ports.portNumber) {
163+
return (
164+
<>
165+
{' '}
166+
{ports.portNumber.protocol}:{ports.portNumber.port}{' '}
167+
</>
168+
);
169+
}
170+
}),
171+
},
172+
{
173+
name: t('translation|From'),
174+
value: item.from?.map(from => {
175+
if (!from.pods && !from.namespaces) {
176+
return <></>;
177+
}
178+
return (
179+
<>
180+
{from.pods ? (
181+
<>
182+
Pods:
183+
{Object.keys(from.pods.namespaceSelector?.matchLabels || {}).length ===
184+
0 &&
185+
Object.keys(from.pods.podSelector?.matchLabels || {}).length === 0 ? (
186+
<Typography sx={metadataStyles} display="inline">
187+
All Pods
188+
</Typography>
189+
) : (
190+
<>
191+
<MetadataDictGrid dict={from.pods.namespaceSelector?.matchLabels} />
192+
<MetadataDictGrid dict={from.pods.podSelector?.matchLabels} />
193+
</>
194+
)}
195+
</>
196+
) : null}
197+
198+
{from.namespaces ? (
199+
<>
200+
Namespaces:
201+
{Object.keys(from.namespaces.matchLabels || {}).length === 0 ? (
202+
<Typography sx={metadataStyles} display="inline">
203+
All Namespaces
204+
</Typography>
205+
) : (
206+
<MetadataDictGrid dict={from.namespaces.matchLabels} />
207+
)}
208+
</>
209+
) : null}
210+
</>
211+
);
212+
}),
213+
},
214+
]}
215+
/>
216+
</SectionBox>
217+
))}
218+
</>
219+
);
220+
}
221+
222+
function Egress(props: { egress: AdminNetworkPolicyEgressRule[] }) {
223+
const { egress } = props;
224+
if (!egress || egress.length === 0) {
225+
return <></>;
226+
}
227+
return (
228+
<>
229+
{egress.map((item: AdminNetworkPolicyEgressRule, index: number) => (
230+
<SectionBox key={`egress-${index}`} title={t('Egress')}>
231+
<NameValueTable
232+
rows={[
233+
{
234+
name: t('Name'),
235+
value: item.name || <Box sx={metadataStyles}>-</Box>,
236+
},
237+
{
238+
name: t('translation|Action'),
239+
value: item.action,
240+
},
241+
{
242+
name: t('translation|To'),
243+
value: item.to?.map(to => {
244+
if (
245+
!to.pods &&
246+
!to.namespaces &&
247+
!to.nodes &&
248+
!to.networks &&
249+
!to.domainNames
250+
) {
251+
return <></>;
252+
}
253+
// return <MetadataDictGrid dict={to.pods?.namespaceSelector?.matchLabels} />
254+
return (
255+
<>
256+
{to.pods ? (
257+
<>
258+
Pods:
259+
{Object.keys(to.pods.namespaceSelector?.matchLabels || {}).length ===
260+
0 &&
261+
Object.keys(to.pods.podSelector?.matchLabels || {}).length === 0 ? (
262+
<Typography sx={metadataStyles} display="inline">
263+
All Pods
264+
</Typography>
265+
) : (
266+
<>
267+
<MetadataDictGrid dict={to.pods.namespaceSelector?.matchLabels} />
268+
<MetadataDictGrid dict={to.pods.podSelector?.matchLabels} />
269+
</>
270+
)}
271+
</>
272+
) : null}
273+
274+
{to.namespaces ? (
275+
<>
276+
Namespaces:
277+
{Object.keys(to.namespaces.matchLabels || {}).length === 0 ? (
278+
<Typography sx={metadataStyles} display="inline">
279+
All Namespaces
280+
</Typography>
281+
) : (
282+
<MetadataDictGrid dict={to.namespaces.matchLabels} />
283+
)}
284+
</>
285+
) : null}
286+
</>
287+
);
288+
}),
289+
},
290+
{
291+
name: t('Ports'),
292+
value: item.ports?.map((ports: AdminNetworkPolicyPort) => {
293+
if (!ports.portNumber) {
294+
return <></>;
295+
}
296+
if (ports.portNumber) {
297+
return (
298+
<>
299+
{' '}
300+
{ports.portNumber.protocol}:{ports.portNumber.port}{' '}
301+
</>
302+
);
303+
}
304+
}),
305+
},
306+
]}
307+
/>
308+
</SectionBox>
309+
))}
310+
</>
311+
);
312+
}
313+
314+
return (
315+
<DetailsGrid
316+
resourceType={AdminNetworkPolicy}
317+
name={name}
318+
subject={subject}
319+
cluster={cluster}
320+
withEvents
321+
extraInfo={item =>
322+
item && [
323+
{
324+
name: t('Subject Selector'),
325+
value: <SubjectSelector AdminNetworkPolicy={item} />,
326+
},
327+
]
328+
}
329+
extraSections={item =>
330+
item && [
331+
{
332+
id: 'adminnetworkpolicy-ingress',
333+
section: <Ingress ingress={item.jsonData.spec.ingress} />,
334+
},
335+
{
336+
id: 'adminnetworkpolicy-egress',
337+
section: <Egress egress={item.jsonData.spec.egress} />,
338+
},
339+
]
340+
}
341+
/>
342+
);
343+
}

0 commit comments

Comments
 (0)