Skip to content

Commit 7c96844

Browse files
authored
IP range add is IPv4 only (#2083)
update IP range add form and validation to be IPv4 only
1 parent b844a42 commit 7c96844

File tree

3 files changed

+57
-54
lines changed

3 files changed

+57
-54
lines changed

app/forms/ip-pool-range-add.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8+
import { type FieldErrors } from 'react-hook-form'
89
import { useNavigate } from 'react-router-dom'
910

1011
import { useApiMutation, useApiQueryClient, type IpRange } from '@oxide/api'
@@ -24,10 +25,7 @@ const defaultValues: IpRange = {
2425

2526
const invalidAddressError = { type: 'pattern', message: 'Not a valid IP address' }
2627

27-
const diffVersionError = {
28-
type: 'custom',
29-
message: 'First and last must be the same version',
30-
}
28+
const ipv6Error = { type: 'pattern', message: 'IPv6 ranges are not yet supported' }
3129

3230
/**
3331
* Pretty straightforward -- make sure IPs are valid and both first and last
@@ -40,21 +38,27 @@ function resolver(values: IpRange) {
4038
const first = validateIp(values.first)
4139
const last = validateIp(values.last)
4240

43-
let errors = undefined
41+
const errors: FieldErrors<IpRange> = {}
42+
43+
if (!first.valid) {
44+
errors.first = invalidAddressError
45+
} else if (first.isv6) {
46+
errors.first = ipv6Error
47+
}
4448

45-
if (!first.valid || !last.valid) {
46-
errors = {
47-
first: first.valid ? undefined : invalidAddressError,
48-
last: last.valid ? undefined : invalidAddressError,
49-
}
50-
} else if ((first.isv4 && last.isv6) || (first.isv6 && last.isv4)) {
51-
errors = { first: diffVersionError, last: diffVersionError }
49+
if (!last.valid) {
50+
errors.last = invalidAddressError
51+
} else if (last.isv6) {
52+
errors.last = ipv6Error
5253
}
5354

54-
// TODO: if we were really cool we could check first <= last but that sounds
55-
// like a pain
55+
// TODO: once we support IPv6 we need to check for version mismatch here
56+
57+
// TODO: if we were really cool we could check first <= last but it would add
58+
// 6k gzipped to the bundle with ip-num
5659

57-
return errors ? { values: {}, errors } : { values, errors: {} }
60+
// no errors
61+
return Object.keys(errors).length > 0 ? { values: {}, errors } : { values, errors: {} }
5862
}
5963

6064
export function IpPoolAddRangeSideModalForm() {
@@ -89,7 +93,7 @@ export function IpPoolAddRangeSideModalForm() {
8993
>
9094
<Message
9195
variant="info"
92-
content="IP ranges are inclusive. Addresses can be either IPv4 or IPv6, but first and last must be the same version, and first must be less than or equal to last."
96+
content="Only IPv4 ranges are currently supported. Ranges are inclusive, and first must be less than or equal to last."
9397
/>
9498
<TextField
9599
name="first"

mock-api/ip-pool.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ export const ipPoolRanges: Json<IpPoolRange[]> = [
7777
},
7878
time_created: new Date().toISOString(),
7979
},
80+
8081
{
8182
id: '7e6e94b9-748e-4219-83a3-cec76253ec70',
8283
ip_pool_id: ipPool2.id,
8384
range: {
84-
first: '10.0.0.33',
85-
last: '10.0.0.38',
85+
first: 'fd00::1',
86+
last: 'fd00::20',
8687
},
8788
time_created: new Date().toISOString(),
8889
},

test/e2e/ip-pools.e2e.ts

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ test('IP pool list', async ({ page }) => {
2323
await expect(table.getByRole('row')).toHaveCount(5) // header + 4 rows
2424

2525
await expectRowVisible(table, { name: 'ip-pool-1', Utilization: '6 / 24' })
26-
await expectRowVisible(table, { name: 'ip-pool-2', Utilization: '0 / 6' })
26+
await expectRowVisible(table, {
27+
name: 'ip-pool-2',
28+
Utilization: 'v4' + '0 / 0' + 'v6' + '0 / 32',
29+
})
2730
await expectRowVisible(table, { name: 'ip-pool-3', Utilization: '0 / 0' })
2831
await expectRowVisible(table, {
2932
name: 'ip-pool-4',
@@ -133,12 +136,13 @@ test('IP pool create', async ({ page }) => {
133136
})
134137

135138
test('IP range validation and add', async ({ page }) => {
136-
await page.goto('/system/networking/ip-pools/ip-pool-1')
139+
await page.goto('/system/networking/ip-pools/ip-pool-2')
137140

138141
// check the utilization bar
139-
await expect(page.getByText('IPv4(IPs)25%')).toBeVisible()
140-
await expect(page.getByText('Allocated6')).toBeVisible()
141-
await expect(page.getByText('Capacity24')).toBeVisible()
142+
await expect(page.getByText('IPv4(IPs)')).toBeHidden()
143+
await expect(page.getByText('IPv6(IPs)0%')).toBeVisible()
144+
await expect(page.getByText('Allocated0')).toBeVisible()
145+
await expect(page.getByText('Capacity32')).toBeVisible()
142146

143147
await page.getByRole('link', { name: 'Add range' }).click()
144148

@@ -148,10 +152,9 @@ test('IP range validation and add', async ({ page }) => {
148152
const submit = dialog.getByRole('button', { name: 'Add IP range' })
149153
const invalidMsg = dialog.getByText('Not a valid IP address')
150154
// exact to differentiate from same text in help message at the top of the form
151-
const sameVersionMsg = dialog.getByText('First and last must be the same version', {
152-
exact: true,
153-
})
155+
const ipv6Msg = dialog.getByText('IPv6 ranges are not yet supported')
154156

157+
const v4Addr = '192.1.2.3'
155158
const v6Addr = '2001:db8::1234:5678'
156159

157160
await expect(dialog).toBeVisible()
@@ -162,50 +165,41 @@ test('IP range validation and add', async ({ page }) => {
162165

163166
await expect(invalidMsg).toHaveCount(2)
164167

165-
// fix last
166-
await last.fill('123.4.56.7')
167-
168-
// first is still bad
168+
// change last to v6, not allowed
169+
await last.fill(v6Addr)
169170
await expect(invalidMsg).toHaveCount(1)
171+
await expect(ipv6Msg).toHaveCount(1)
170172

171-
// change first to a valid ipv6
173+
// change first to v6, still not allowed
172174
await first.fill(v6Addr)
173-
174-
// now we get the error about the same version on first because it has had
175-
// an error, so it is now validating onChange, but it doesn't show up on last
176-
// until we try to submit
177-
await expect(sameVersionMsg).toHaveCount(1)
175+
await expect(ipv6Msg).toHaveCount(2)
178176
await expect(invalidMsg).toBeHidden()
179177

180-
await submit.click()
181-
await expect(sameVersionMsg).toHaveCount(2)
182-
183-
// now make last also a v6 and we're good
184-
await last.fill(v6Addr)
185-
186-
// actually first's error doesn't disappear until we blur it or submit
187-
await expect(sameVersionMsg).toHaveCount(1)
188-
await expect(invalidMsg).toBeHidden()
178+
// now make first v4, then last
179+
await first.fill(v4Addr)
180+
await expect(ipv6Msg).toHaveCount(1)
181+
await last.fill(v4Addr)
182+
await expect(ipv6Msg).toBeHidden()
189183

190184
await submit.click()
191185
await expect(dialog).toBeHidden()
192186

193187
const table = page.getByRole('table')
194-
await expectRowVisible(table, { First: v6Addr, Last: v6Addr })
188+
await expectRowVisible(table, { First: v4Addr, Last: v4Addr })
195189

196190
// now the utilization bars are split in two
197-
await expect(page.getByText('IPv4(IPs)25%')).toBeVisible()
198-
await expect(page.getByText('Allocated6')).toBeVisible()
199-
await expect(page.getByText('Capacity24')).toBeVisible()
200-
await expect(page.getByText('IPv6(IPs)0%')).toBeVisible()
201-
await expect(page.getByText('Allocated0')).toBeVisible()
191+
await expect(page.getByText('IPv4(IPs)0%')).toBeVisible()
192+
await expect(page.getByText('Allocated0')).toHaveCount(2)
202193
await expect(page.getByText('Capacity1')).toBeVisible()
203194

195+
await expect(page.getByText('IPv6(IPs)0%')).toBeVisible()
196+
await expect(page.getByText('Capacity32')).toBeVisible()
197+
204198
// go back to the pool and verify the utilization column changed
205199
await page.getByRole('link', { name: 'Networking' }).click()
206200
await expectRowVisible(table, {
207-
name: 'ip-pool-1',
208-
Utilization: 'v4' + '6 / 24' + 'v6' + '0 / 1',
201+
name: 'ip-pool-2',
202+
Utilization: 'v4' + '0 / 1' + 'v6' + '0 / 32',
209203
})
210204
})
211205

@@ -264,10 +258,14 @@ test('deleting floating IP decrements utilization', async ({ page }) => {
264258
})
265259

266260
test('no ranges means no utilization bar', async ({ page }) => {
267-
await page.goto('/system/networking/ip-pools/ip-pool-2')
261+
await page.goto('/system/networking/ip-pools/ip-pool-1')
268262
await expect(page.getByText('IPv4(IPs)')).toBeVisible()
269263
await expect(page.getByText('IPv6(IPs)')).toBeHidden()
270264

265+
await page.goto('/system/networking/ip-pools/ip-pool-2')
266+
await expect(page.getByText('IPv4(IPs)')).toBeHidden()
267+
await expect(page.getByText('IPv6(IPs)')).toBeVisible()
268+
271269
await page.goto('/system/networking/ip-pools/ip-pool-3')
272270
await expect(page.getByText('IPv4(IPs)')).toBeHidden()
273271
await expect(page.getByText('IPv6(IPs)')).toBeHidden()

0 commit comments

Comments
 (0)