Skip to content

Commit 29272b1

Browse files
committed
frontend: network: Add support for Admin Network Policy resources
This change adds CRUD support for ANP.
1 parent 346fb6a commit 29272b1

File tree

5 files changed

+592
-0
lines changed

5 files changed

+592
-0
lines changed

frontend/src/components/Sidebar/useSidebarItems.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ export const useSidebarItems = (sidebarName: string = DefaultSidebars.IN_CLUSTER
216216
name: 'NetworkPolicies',
217217
label: t('glossary|Network Policies'),
218218
},
219+
{
220+
name: 'AdminNetworkPolicies',
221+
label: t('glossary|Admin Network Policies (alpha)'),
222+
},
219223
],
220224
},
221225
{
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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 Typography from '@mui/material/Typography';
19+
import { useTranslation } from 'react-i18next';
20+
import { useParams } from 'react-router';
21+
import { matchExpressionSimplifier, matchLabelsSimplifier } from '../../lib/k8s';
22+
import AdminNetworkPolicy, {
23+
AdminNetworkPolicyEgressRule,
24+
AdminNetworkPolicyIngressRule,
25+
AdminNetworkPolicyPort,
26+
} from '../../lib/k8s/adminnetworkpolicy';
27+
import { LabelSelector } from '../../lib/k8s/cluster';
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+
33+
export default function AdminNetworkPolicyDetails(props: {
34+
name?: string;
35+
cluster?: string;
36+
subject?: string;
37+
}) {
38+
const params = useParams<{ subject: string; name: string }>();
39+
const { name = params.name, subject = params.subject, cluster } = props;
40+
const { t } = useTranslation(['glossary', 'translation']);
41+
42+
function prepareMatchLabelsAndExpressions(
43+
type: 'Pods' | 'Namespaces',
44+
matchLabels: LabelSelector['matchLabels'],
45+
matchExpressions: LabelSelector['matchExpressions']
46+
) {
47+
const matchLabelsSimplified = matchLabelsSimplifier(matchLabels) || [];
48+
const matchExpressionsSimplified = matchExpressionSimplifier(matchExpressions) || [];
49+
if (matchLabels === undefined && matchExpressions === undefined) {
50+
return <>{type}: All</>;
51+
}
52+
53+
return (
54+
<>
55+
{matchLabelsSimplified.map(label => (
56+
<Typography sx={metadataStyles} display="inline">
57+
{label}
58+
</Typography>
59+
))}
60+
{matchExpressionsSimplified.map(expression => (
61+
<Typography sx={metadataStyles} display="inline">
62+
{expression}
63+
</Typography>
64+
))}
65+
</>
66+
);
67+
}
68+
69+
function SubjectSelector(props: { AdminNetworkPolicy: AdminNetworkPolicy }) {
70+
const { AdminNetworkPolicy } = props;
71+
// this is an exception which should never happen, but we handle it gracefully
72+
if (
73+
AdminNetworkPolicy.jsonData?.spec?.subject?.pods === undefined &&
74+
AdminNetworkPolicy.jsonData?.spec?.subject?.namespaces === undefined
75+
) {
76+
return <></>;
77+
}
78+
79+
if (AdminNetworkPolicy.jsonData?.spec?.subject?.pods !== undefined) {
80+
return prepareMatchLabelsAndExpressions(
81+
'Pods',
82+
AdminNetworkPolicy.jsonData?.spec?.subject?.pods?.podSelector?.matchLabels,
83+
AdminNetworkPolicy.jsonData?.spec?.subject?.pods?.podSelector?.matchExpressions
84+
);
85+
}
86+
87+
if (AdminNetworkPolicy.jsonData?.spec?.subject?.namespaces !== undefined) {
88+
return prepareMatchLabelsAndExpressions(
89+
'Namespaces',
90+
AdminNetworkPolicy.jsonData?.spec?.subject?.namespaces?.podSelector?.matchLabels,
91+
AdminNetworkPolicy.jsonData?.spec?.subject?.namespaces?.podSelector?.matchExpressions
92+
);
93+
}
94+
}
95+
96+
function Ingress(props: { ingress: AdminNetworkPolicyIngressRule[] }) {
97+
const { ingress } = props;
98+
99+
if (!ingress || ingress.length === 0) {
100+
return <></>;
101+
}
102+
103+
return (
104+
<>
105+
{ingress.map((item: AdminNetworkPolicyIngressRule, index: number) => (
106+
<SectionBox key={`ingress-${index}`} title={t('Ingress')}>
107+
<NameValueTable
108+
rows={[
109+
{
110+
name: t('Name'),
111+
value: item.name || <Box sx={metadataStyles}>-</Box>,
112+
},
113+
{
114+
name: t('translation|Action'),
115+
value: (
116+
<>
117+
<span color="success">{item.action}</span>
118+
</>
119+
),
120+
},
121+
{
122+
name: t('Ports'),
123+
value: item.ports?.map((ports: AdminNetworkPolicyPort) => {
124+
if (!ports.portNumber) {
125+
return <></>;
126+
}
127+
if (ports.portNumber) {
128+
return (
129+
<>
130+
{' '}
131+
{ports.portNumber.protocol}:{ports.portNumber.port}{' '}
132+
</>
133+
);
134+
}
135+
}),
136+
},
137+
{
138+
name: t('translation|From'),
139+
value: item.from?.map(from => {
140+
if (!from.pods && !from.namespaces) {
141+
return <></>;
142+
}
143+
return (
144+
<>
145+
{from.pods ? (
146+
<>
147+
Pods:
148+
{Object.keys(from.pods.namespaceSelector?.matchLabels || {}).length ===
149+
0 &&
150+
Object.keys(from.pods.podSelector?.matchLabels || {}).length === 0 ? (
151+
<Typography sx={metadataStyles} display="inline">
152+
All Pods
153+
</Typography>
154+
) : (
155+
<>
156+
<MetadataDictGrid dict={from.pods.namespaceSelector?.matchLabels} />
157+
<MetadataDictGrid dict={from.pods.podSelector?.matchLabels} />
158+
</>
159+
)}
160+
</>
161+
) : null}
162+
163+
{from.namespaces ? (
164+
<>
165+
Namespaces:
166+
{Object.keys(from.namespaces.matchLabels || {}).length === 0 ? (
167+
<Typography sx={metadataStyles} display="inline">
168+
All Namespaces
169+
</Typography>
170+
) : (
171+
<MetadataDictGrid dict={from.namespaces.matchLabels} />
172+
)}
173+
</>
174+
) : null}
175+
</>
176+
);
177+
}),
178+
},
179+
]}
180+
/>
181+
</SectionBox>
182+
))}
183+
</>
184+
);
185+
}
186+
187+
function Egress(props: { egress: AdminNetworkPolicyEgressRule[] }) {
188+
const { egress } = props;
189+
if (!egress || egress.length === 0) {
190+
return <></>;
191+
}
192+
return (
193+
<>
194+
{egress.map((item: AdminNetworkPolicyEgressRule, index: number) => (
195+
<SectionBox key={`egress-${index}`} title={t('Egress')}>
196+
<NameValueTable
197+
rows={[
198+
{
199+
name: t('Name'),
200+
value: item.name || <Box sx={metadataStyles}>-</Box>,
201+
},
202+
{
203+
name: t('translation|Action'),
204+
value: item.action,
205+
},
206+
{
207+
name: t('translation|To'),
208+
value: item.to?.map(to => {
209+
if (
210+
!to.pods &&
211+
!to.namespaces &&
212+
!to.nodes &&
213+
!to.networks &&
214+
!to.domainNames
215+
) {
216+
return <></>;
217+
}
218+
// return <MetadataDictGrid dict={to.pods?.namespaceSelector?.matchLabels} />
219+
return (
220+
<>
221+
{to.pods ? (
222+
<>
223+
Pods:
224+
{Object.keys(to.pods.namespaceSelector?.matchLabels || {}).length ===
225+
0 &&
226+
Object.keys(to.pods.podSelector?.matchLabels || {}).length === 0 ? (
227+
<Typography sx={metadataStyles} display="inline">
228+
All Pods
229+
</Typography>
230+
) : (
231+
<>
232+
<MetadataDictGrid dict={to.pods.namespaceSelector?.matchLabels} />
233+
<MetadataDictGrid dict={to.pods.podSelector?.matchLabels} />
234+
</>
235+
)}
236+
</>
237+
) : null}
238+
239+
{to.namespaces ? (
240+
<>
241+
Namespaces:
242+
{Object.keys(to.namespaces.matchLabels || {}).length === 0 ? (
243+
<Typography sx={metadataStyles} display="inline">
244+
All Namespaces
245+
</Typography>
246+
) : (
247+
<MetadataDictGrid dict={to.namespaces.matchLabels} />
248+
)}
249+
</>
250+
) : null}
251+
</>
252+
);
253+
}),
254+
},
255+
{
256+
name: t('Ports'),
257+
value: item.ports?.map((ports: AdminNetworkPolicyPort) => {
258+
if (!ports.portNumber) {
259+
return <></>;
260+
}
261+
if (ports.portNumber) {
262+
return (
263+
<>
264+
{' '}
265+
{ports.portNumber.protocol}:{ports.portNumber.port}{' '}
266+
</>
267+
);
268+
}
269+
}),
270+
},
271+
]}
272+
/>
273+
</SectionBox>
274+
))}
275+
</>
276+
);
277+
}
278+
279+
return (
280+
<DetailsGrid
281+
resourceType={AdminNetworkPolicy}
282+
name={name}
283+
subject={subject}
284+
cluster={cluster}
285+
withEvents
286+
extraInfo={item =>
287+
item && [
288+
{
289+
name: t('Subject Selector'),
290+
value: <SubjectSelector AdminNetworkPolicy={item} />,
291+
},
292+
]
293+
}
294+
extraSections={item =>
295+
item && [
296+
{
297+
id: 'adminnetworkpolicy-ingress',
298+
section: <Ingress ingress={item.jsonData.spec.ingress} />,
299+
},
300+
{
301+
id: 'adminnetworkpolicy-egress',
302+
section: <Egress egress={item.jsonData.spec.egress} />,
303+
},
304+
]
305+
}
306+
/>
307+
);
308+
}

0 commit comments

Comments
 (0)