Skip to content

Commit 895ace7

Browse files
committed
Merge remote-tracking branch 'origin/main' into dependabot/docker/docker-dependencies-1784aaf028
2 parents f089cae + 5e9f92b commit 895ace7

6 files changed

Lines changed: 91 additions & 33 deletions

src/pages/network-policies/create-edit/NetworkPoliciesEgressCreateEditPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from 'redux/otomiApi'
2222
import { useTranslation } from 'react-i18next'
2323
import { Divider } from 'components/Divider'
24+
import { isEqual } from 'lodash'
2425
import { createEgressSchema } from './create-edit-networkPolicies.validator'
2526
import { useStyles } from './create-edit-networkPolicies.styles'
2627
import NetworkPolicyEgressPortRow from './NetworkPolicyEgressPortRow'
@@ -178,8 +179,8 @@ export default function NetworkPoliciesEgressCreateEditPage({
178179
variant='contained'
179180
color='primary'
180181
loading={isCreating || isUpdating}
181-
disabled={busy}
182-
sx={{ float: 'right' }}
182+
disabled={busy || isEqual(data, watch())}
183+
sx={{ float: 'right', textTransform: 'none' }}
183184
>
184185
{networkPolicyName ? 'Save Changes' : 'Create Outbound Rule'}
185186
</LoadingButton>

src/pages/network-policies/create-edit/NetworkPoliciesIngressCreateEditPage.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Delete as DeleteIcon } from '@mui/icons-material'
2323
import { useCallback, useEffect, useState } from 'react'
2424
import { useTranslation } from 'react-i18next'
2525
import InformationBanner from 'components/InformationBanner'
26+
import { isEqual } from 'lodash'
2627
import { useStyles } from './create-edit-networkPolicies.styles'
2728
import { createIngressSchema } from './create-edit-networkPolicies.validator'
2829
import NetworkPolicyPodLabelRow from './NetworkPolicyPodLabelRow'
@@ -152,7 +153,7 @@ export default function NetworkPoliciesIngressCreateEditPage({
152153
{sourceFields.map((field, index) => (
153154
<div key={field.id} style={{ display: 'flex', alignItems: 'flex-end', gap: '8px' }}>
154155
<NetworkPolicyPodLabelRow
155-
aplWorkloads={aplWorkloads}
156+
aplWorkloads={aplWorkloads || []}
156157
teamId={teamId}
157158
rowIndex={index}
158159
fieldArrayName={`ruleType.ingress.allow.${index}`}
@@ -191,7 +192,7 @@ export default function NetworkPoliciesIngressCreateEditPage({
191192

192193
{/* Target row needs seperate component becuase of data shape */}
193194
<NetworkPolicyTargetLabelRow
194-
aplWorkloads={aplWorkloads}
195+
aplWorkloads={aplWorkloads || []}
195196
teamId={teamId}
196197
prefixName='ruleType.ingress'
197198
showBanner={toggleShowMultiPodInformationBanner}
@@ -216,7 +217,7 @@ export default function NetworkPoliciesIngressCreateEditPage({
216217
variant='contained'
217218
color='primary'
218219
loading={isLoadingCreate || isLoadingUpdate}
219-
disabled={isLoadingCreate || isLoadingUpdate || isLoadingDelete}
220+
disabled={isLoadingCreate || isLoadingUpdate || isLoadingDelete || isEqual(data, watch())}
220221
sx={{ float: 'right', textTransform: 'none' }}
221222
>
222223
{networkPolicyName ? 'Save Changes' : 'Create Inbound Rule'}

src/pages/network-policies/create-edit/NetworkPolicyPodLabelMatchHelper.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ interface WorkloadOption {
1717
* @returns A PodLabelMatch with the selected label name and value, or null if none matched.
1818
*/
1919
export function getDefaultPodLabel(workloadName: string, podLabels: Record<string, string>): PodLabelMatch | null {
20+
if (!workloadName || !podLabels || typeof podLabels !== 'object') return null
21+
2022
// 1. Exact match on app.kubernetes.io/instance
2123
const instanceKey = 'app.kubernetes.io/instance'
2224
const instanceValue = podLabels[instanceKey]
@@ -28,16 +30,16 @@ export function getDefaultPodLabel(workloadName: string, podLabels: Record<strin
2830
if (componentValue === 'rabbitmq') return { name: instanceKey, value: `${workloadName}-${componentValue}` }
2931

3032
// 3. Knative serving label
31-
const knativeKey = Object.keys(podLabels).find((key) => key.startsWith('serving.knative.dev/service'))
33+
const knativeKey = Object.keys(podLabels).find((key) => key?.startsWith('serving.knative.dev/service'))
3234
if (knativeKey) return { name: knativeKey, value: podLabels[knativeKey] }
3335

3436
// 4. cnpg cluster label
35-
const cnpgKey = Object.keys(podLabels).find((key) => key.startsWith('cnpg.io/cluster'))
37+
const cnpgKey = Object.keys(podLabels).find((key) => key?.startsWith('cnpg.io/cluster'))
3638
if (cnpgKey) return { name: cnpgKey, value: podLabels[cnpgKey] }
3739

3840
// 5. Istio canonical name for ksvc workloads
3941
const istioKey = 'service.istio.io/canonical-name'
40-
if (workloadName.startsWith('ksvc-')) {
42+
if (workloadName?.startsWith('ksvc-')) {
4143
const istioValue = podLabels[istioKey]
4244
if (istioValue === workloadName) return { name: istioKey, value: istioValue }
4345
}
@@ -55,7 +57,7 @@ export function getInitialActiveWorkloadRow(
5557
namespaceValue: string,
5658
workloads: GetAllAplWorkloadNamesApiResponse,
5759
): WorkloadOption {
58-
if (!labelValue) return { name: 'unknown', namespace: '' }
60+
if (!labelValue || !workloads || !Array.isArray(workloads)) return { name: 'unknown', namespace: '' }
5961

6062
const normalizedLabel = normalizeRabbitMQLabel(labelValue)
6163
const matches = workloads.filter(
@@ -73,7 +75,7 @@ export function getInitialActiveWorkloadTarget(
7375
labelValue: string,
7476
workloads: GetAllAplWorkloadNamesApiResponse,
7577
): string {
76-
if (!labelValue) return 'unknown'
78+
if (!labelValue || !workloads || !Array.isArray(workloads)) return 'unknown'
7779

7880
const normalizedLabel = normalizeRabbitMQLabel(labelValue)
7981
const matches = workloads.filter((w) => w.metadata.name === normalizedLabel)

src/pages/network-policies/create-edit/NetworkPolicyPodLabelRow.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ export default function NetworkPolicyPodLabelRow({
4949
const [activeWorkload, setActiveWorkload] = useState<WorkloadOption | null>(null)
5050
const [circuitBreaker, setCircuitBreaker] = useState<boolean>(true)
5151

52-
const arrayError = (errors.ruleType?.ingress.allow?.root as any)?.message as string | undefined
52+
const arrayError = (errors.ruleType?.ingress?.allow?.root as any)?.message as string | undefined
5353

5454
// build and sort workload options
5555
const workloadOptions = useMemo(
5656
() =>
5757
aplWorkloads
58-
.map((w) => ({ name: w.metadata.name, namespace: w.metadata.namespace }))
59-
.sort((a, b) => a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name)),
58+
?.map((w) => ({ name: w.metadata.name, namespace: w.metadata.namespace }))
59+
?.sort((a, b) => a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name)),
6060
[aplWorkloads],
6161
)
6262

@@ -75,7 +75,7 @@ export default function NetworkPolicyPodLabelRow({
7575
const { fromLabelValue, fromNamespace } = field.value as PodLabelMatch
7676
if (fromLabelValue) {
7777
const initialActiveWorkload = getInitialActiveWorkloadRow(fromLabelValue, fromNamespace, aplWorkloads)
78-
if (initialActiveWorkload.name === 'unknown' || initialActiveWorkload.name === 'multiple') showBanner()
78+
if (initialActiveWorkload.name === 'unknown' || initialActiveWorkload.name === 'multiple') showBanner?.()
7979
setActiveWorkload(initialActiveWorkload)
8080
} else setCircuitBreaker(false)
8181
}, [])
@@ -90,7 +90,9 @@ export default function NetworkPolicyPodLabelRow({
9090
useEffect(() => {
9191
if (podLabels && circuitBreaker) setCircuitBreaker(false)
9292
if (!activeWorkload || !podLabels) return
93-
const match = getDefaultPodLabel(activeWorkload.name, podLabels)
93+
const currentValue = field.value as PodLabelMatch
94+
if (currentValue?.fromLabelName && currentValue?.fromLabelValue) return
95+
const match = getDefaultPodLabel(activeWorkload.name, podLabels as Record<string, string>)
9496
if (match) {
9597
field.onChange({
9698
fromNamespace: activeWorkload.namespace,
@@ -111,7 +113,7 @@ export default function NetworkPolicyPodLabelRow({
111113
groupBy={(opt) => opt.namespace}
112114
getOptionLabel={(opt) => opt.name}
113115
value={activeWorkload}
114-
onChange={(_e, opt) => setActiveWorkload({ name: opt?.name, namespace: opt?.namespace })}
116+
onChange={(_e, opt) => setActiveWorkload(opt ? { name: opt?.name, namespace: opt?.namespace } : null)}
115117
/>
116118

117119
<Autocomplete
@@ -121,11 +123,15 @@ export default function NetworkPolicyPodLabelRow({
121123
multiple={false}
122124
errorText={arrayError && rowIndex === 0 ? arrayError : ''}
123125
helperText={arrayError && rowIndex === 0 ? arrayError : ''}
124-
options={Object.entries((podLabels as Record<string, string>) ?? {}).map(([k, v]) => `${k}:${v}`)}
125-
value={field.value?.fromLabelName ? `${field.value.fromLabelName}:${field.value.fromLabelValue ?? ''}` : null}
126+
options={
127+
podLabels && typeof podLabels === 'object'
128+
? Object.entries(podLabels as Record<string, string>).map(([k, v]) => `${k}=${v}`)
129+
: []
130+
}
131+
value={field.value?.fromLabelName ? `${field.value.fromLabelName}=${field.value.fromLabelValue ?? ''}` : null}
126132
onChange={(_e, raw: string | null) => {
127-
const [name, value] = raw?.split(':', 2) ?? []
128-
field.onChange({ fromNamespace: activeWorkload.namespace, fromLabelName: name, fromLabelValue: value })
133+
const [name, value] = raw?.split('=', 2) ?? []
134+
field.onChange({ fromNamespace: activeWorkload?.namespace, fromLabelName: name, fromLabelValue: value })
129135
}}
130136
/>
131137
</FormRow>

src/pages/network-policies/create-edit/NetworkPolicyTargetPodLabelRow.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,46 +51,46 @@ export default function NetworkPolicyTargetLabelRow({
5151
// build workload options, but only those in this team’s namespace
5252
const workloadOptions = useMemo<WorkloadOption[]>(() => {
5353
return aplWorkloads
54-
.map((w) => ({ name: w.metadata.name, namespace: w.metadata.namespace }))
55-
.filter((o) => o.namespace === `team-${teamId}`)
56-
.sort((a, b) => a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name))
54+
?.map((w) => ({ name: w.metadata.name, namespace: w.metadata.namespace }))
55+
?.filter((o) => o.namespace === `team-${teamId}`)
56+
?.sort((a, b) => a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name))
5757
}, [aplWorkloads, teamId])
5858

5959
// find the currently selected option object
60-
const selectedWorkloadOption = workloadOptions.find((o) => o.name === activeWorkload) || null
60+
const selectedWorkloadOption = workloadOptions?.find((o) => o.name === activeWorkload) || null
6161

6262
// fetch the label→value map for that pod spec
6363
const { data: podLabels } = useGetK8SWorkloadPodLabelsQuery(
6464
{ teamId, workloadName: activeWorkload, namespace: `team-${teamId}` },
6565
{ skip: !activeWorkload },
6666
)
67-
const labelOptions = useMemo(() => Object.entries(podLabels ?? {}).map(([k, v]) => `${k}:${v}`), [podLabels])
67+
const labelOptions = useMemo(() => Object.entries(podLabels ?? {}).map(([k, v]) => `${k}=${v}`), [podLabels])
6868

6969
// Initial edit-mode circuitbreaker, prevent prepopulated fields from starting a rerender loop
7070
useEffect(() => {
7171
if (toValue && circuitBreaker) {
7272
setCircuitBreaker(false)
7373
if (isEditMode) {
7474
const initialActiveWorkload = getInitialActiveWorkloadTarget(toValue, aplWorkloads)
75-
if (initialActiveWorkload === 'unknown' || initialActiveWorkload === 'multiple') showBanner()
75+
if (initialActiveWorkload === 'unknown' || initialActiveWorkload === 'multiple') showBanner?.()
7676
else setActiveWorkload(initialActiveWorkload)
7777
}
7878
}
7979
}, [toValue])
8080

81-
// once podLabels arrive, and no explicit toName, apply default
81+
// once podLabels arrive, and no explicit toName, apply default (only on initial load, not after user clears)
8282
useEffect(() => {
83-
if (activeWorkload && podLabels) {
83+
if (activeWorkload && podLabels && !toName && !toValue && circuitBreaker) {
8484
const match = getDefaultPodLabel(activeWorkload, podLabels)
8585
if (match) {
8686
setValue(`${prefixName}.toLabelName`, match.name)
8787
setValue(`${prefixName}.toLabelValue`, match.value)
8888
}
8989
}
90-
}, [activeWorkload, podLabels])
90+
}, [activeWorkload, podLabels, toName, toValue, circuitBreaker])
9191

9292
// once the user has picked or defaulted a toName/toValue, fetch matching pod names
93-
const rawSelector = toName && toValue ? `${toName}:${toValue}` : ''
93+
const rawSelector = toName && toValue ? `${toName}=${toValue}` : ''
9494

9595
return (
9696
<FormRow spacing={10}>
@@ -104,6 +104,9 @@ export default function NetworkPolicyTargetLabelRow({
104104
value={selectedWorkloadOption}
105105
onChange={(_e, opt) => {
106106
setActiveWorkload(opt?.name ?? '')
107+
if (!opt) setCircuitBreaker(false)
108+
setValue(`${prefixName}.toLabelName`, '')
109+
setValue(`${prefixName}.toLabelValue`, '')
107110
}}
108111
/>
109112

@@ -117,9 +120,10 @@ export default function NetworkPolicyTargetLabelRow({
117120
if (!newVal) {
118121
setValue(`${prefixName}.toLabelName`, '')
119122
setValue(`${prefixName}.toLabelValue`, '')
123+
setCircuitBreaker(false)
120124
return
121125
}
122-
const [name, value] = newVal.split(':', 2)
126+
const [name, value] = newVal.split('=', 2) ?? []
123127
setValue(`${prefixName}.toLabelName`, name)
124128
setValue(`${prefixName}.toLabelValue`, value)
125129
}}

src/pages/network-policies/create-edit/create-edit-networkPolicies.validator.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@ export interface CreateEgressNetpolApiResponse {
4747
export const createIngressSchema = yup.object({
4848
id: yup.string().optional(),
4949
teamId: yup.string().optional(),
50-
name: yup.string().required('Inbound rule name is required').max(24, 'Name must not exceed 24 characters'),
50+
name: yup
51+
.string()
52+
.required('Inbound rule name is required')
53+
.max(24, 'Name must not exceed 24 characters')
54+
.matches(
55+
/^[a-z](?:[a-z0-9-]*[a-z0-9])?$/,
56+
'Name must start with a lowercase letter, contain only lowercase letters, numbers, and hyphens, and end with a letter or number',
57+
),
5158
ruleType: yup
5259
.object({
5360
type: yup.mixed<IngressRuleType['type']>().oneOf(['ingress']).required(),
@@ -89,7 +96,14 @@ export const createIngressSchema = yup.object({
8996
export const createEgressSchema = yup.object({
9097
id: yup.string().optional(),
9198
teamId: yup.string().optional(),
92-
name: yup.string().required('Name is required').max(24, 'Name must not exceed 24 characters'),
99+
name: yup
100+
.string()
101+
.required('Name is required')
102+
.max(24, 'Name must not exceed 24 characters')
103+
.matches(
104+
/^[a-z](?:[a-z0-9-]*[a-z0-9])?$/,
105+
'Name must start with a lowercase letter, contain only lowercase letters, numbers, and hyphens, and end with a letter or number',
106+
),
93107
ruleType: yup
94108
.object({
95109
type: yup.mixed<EgressRuleType['type']>().oneOf(['egress']).required(),
@@ -115,6 +129,36 @@ export const createEgressSchema = yup.object({
115129
.required('Protocol is required'),
116130
}),
117131
)
132+
.test('unique-ports', function (ports) {
133+
if (!ports || ports.length <= 1) return true
134+
135+
const portNumbers: number[] = []
136+
const duplicateIndexes: number[] = []
137+
138+
// Find all duplicate port numbers and their indexes
139+
ports.forEach((port: any, index: number) => {
140+
if (port?.number) {
141+
const existingIndex = portNumbers.indexOf(port.number as number)
142+
if (existingIndex !== -1) {
143+
// Mark both the original and current as duplicates
144+
if (!duplicateIndexes.includes(existingIndex)) duplicateIndexes.push(existingIndex)
145+
duplicateIndexes.push(index)
146+
} else portNumbers.push(port.number as number)
147+
}
148+
})
149+
150+
if (duplicateIndexes.length === 0) return true
151+
152+
// Create errors for each duplicate port
153+
const errors = duplicateIndexes.map((index) =>
154+
this.createError({
155+
path: `${this.path}[${index}].number`,
156+
message: 'Port number is already used',
157+
}),
158+
)
159+
160+
return new yup.ValidationError(errors)
161+
})
118162
.optional(),
119163
})
120164
.required(),

0 commit comments

Comments
 (0)