Skip to content

Commit ff9af79

Browse files
authored
Merge pull request #182 from projectsyn/feat/component-lib/ipcalc
Expose `ipcalc.libsonnet` as a component library
2 parents 7df5f9d + cbc69c7 commit ff9af79

File tree

9 files changed

+397
-68
lines changed

9 files changed

+397
-68
lines changed

.cruft.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"name": "Cilium",
88
"slug": "cilium",
99
"parameter_key": "cilium",
10-
"test_cases": "defaults helm-opensource olm-opensource egress-gateway bgp-control-plane kubeproxyreplacement-strict l2-announcement clustermesh enterprise-bgp hubble-access",
10+
"test_cases": "defaults helm-opensource olm-opensource egress-gateway bgp-control-plane kubeproxyreplacement-strict l2-announcement clustermesh enterprise-bgp hubble-access lib-ipcalc",
1111
"add_lib": "n",
1212
"add_pp": "n",
1313
"add_golden": "y",

.github/workflows/test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
- clustermesh
4343
- enterprise-bgp
4444
- hubble-access
45+
- lib-ipcalc
4546
defaults:
4647
run:
4748
working-directory: ${{ env.COMPONENT_NAME }}
@@ -66,6 +67,7 @@ jobs:
6667
- clustermesh
6768
- enterprise-bgp
6869
- hubble-access
70+
- lib-ipcalc
6971
defaults:
7072
run:
7173
working-directory: ${{ env.COMPONENT_NAME }}

Makefile.vars.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ KUBENT_IMAGE ?= ghcr.io/doitintl/kube-no-trouble:latest
5757
KUBENT_DOCKER ?= $(DOCKER_CMD) $(DOCKER_ARGS) $(root_volume) --entrypoint=/app/kubent $(KUBENT_IMAGE)
5858

5959
instance ?= defaults
60-
test_instances = tests/defaults.yml tests/helm-opensource.yml tests/olm-opensource.yml tests/egress-gateway.yml tests/bgp-control-plane.yml tests/kubeproxyreplacement-strict.yml tests/l2-announcement.yml tests/clustermesh.yml tests/enterprise-bgp.yml tests/hubble-access.yml
60+
test_instances = tests/defaults.yml tests/helm-opensource.yml tests/olm-opensource.yml tests/egress-gateway.yml tests/bgp-control-plane.yml tests/kubeproxyreplacement-strict.yml tests/l2-announcement.yml tests/clustermesh.yml tests/enterprise-bgp.yml tests/hubble-access.yml tests/lib-ipcalc.yml

component/espejote-templates/ipcalc.libsonnet

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../lib/cilium-ipcalc.libsonnet

lib/cilium-ipcalc.libsonnet

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// NOTE(sg): This file is symlinked to `component/espejote-templates` in
2+
// component-cilium to allow the `espejote-templates/egress-gateway.libsonnet`
3+
// library to work regardless of whether it's used by Espejote or the
4+
// component. We export this as a component library since it might be useful
5+
// for other components on Cilium-enabled clusters.
6+
7+
// Convert an IPv4 address in A.B.C.D format that's already been split into an
8+
// array to decimal format according to the formula `A*256^3 + B*256^2 + C*256
9+
// + D`. The decimal format allows us to make range comparisons and compute
10+
// offsets into a range.
11+
// Parameter ip can either be the IP as a string, or already split into an
12+
// array holding each dotted part.
13+
local ipval(ip) =
14+
local iparr =
15+
if std.type(ip) == 'array' then
16+
ip
17+
else
18+
std.split(ip, '.');
19+
local iparr_int = std.map(std.parseInt, iparr);
20+
21+
if std.any(std.map(function(v) v > 255, iparr_int)) then
22+
error 'Error parsing IPv4 address: %s is not a valid address' % [
23+
ip,
24+
]
25+
else
26+
std.foldl(
27+
function(v, p) v * 256 + p,
28+
iparr_int,
29+
0
30+
);
31+
32+
// Extract start and end from the provided range, stripping any
33+
// whitespace. `prefix` is only used for the error message.
34+
local parse_ip_range(prefix, rangespec) =
35+
local range_parts = std.map(
36+
function(s) std.stripChars(s, ' '),
37+
std.split(rangespec, '-')
38+
);
39+
if std.length(range_parts) != 2 then
40+
error 'Expected IP range for "%s" in format "192.0.2.32-192.0.2.63", got %s' % [
41+
prefix,
42+
rangespec,
43+
]
44+
else
45+
{
46+
start: range_parts[0],
47+
end: range_parts[1],
48+
};
49+
50+
local format_ipval(val) =
51+
assert
52+
val >= 0 && val <= ipval('255.255.255.255')
53+
: '%s not an IPv4 address in decimal' % val;
54+
55+
local iparr = std.reverse(std.foldl(
56+
function(st, i)
57+
local arr = st.arr;
58+
local rem = st.rem;
59+
{
60+
arr: arr + [ rem % 256 ],
61+
rem: rem / 256,
62+
},
63+
[ 0, 0, 0, 0 ],
64+
{ arr: [], rem: val }
65+
).arr);
66+
67+
std.join('.', std.map(function(v) '%d' % v, iparr));
68+
69+
// Parse network in CIDR notation. Leading and trailing whitespace is
70+
// stripped. `prefix` is only used for the error message.
71+
//
72+
// This function correctly parses the full network info from arbitrary IPs in
73+
// CIDR notation. We return an object that's inspired by the output of the
74+
// Linux utility `ipcalc`.
75+
//
76+
// The return value contains the network address, broadcast address, count of
77+
// IPs in the CIDR and prefix length. For prefix lengths of less than 32, the
78+
// return value additionally contains the first and last host (in `min_host`
79+
// and `max_host`) and netmask.
80+
local parse_cidr(prefix, cidr) =
81+
local parts = std.split(std.stripChars(cidr, ' '), '/');
82+
if std.length(parts) != 2 then
83+
error 'Expected value for "%s" to be in CIDR notation, got "%s"' % [
84+
prefix,
85+
cidr,
86+
]
87+
else
88+
local prefix_length = std.parseInt(parts[1]);
89+
if prefix_length < 0 || prefix_length > 32 then
90+
error 'Invalid CIDR %s: prefix must be between 0 and 32' % cidr
91+
else
92+
// We compute count, netmask and network address using bitwise operations.
93+
// Jsonnet uses 64 bit integers for bitwise ops, so we don't have to worry
94+
// about overflowing when working with 32 bit values (IPv4 addresses).
95+
//
96+
// IPv4 CIDR notation works as follows: <addr>/<prefix> defines a network
97+
// where the first <prefix> bits of the IP are the "network" and the last
98+
// 32-<prefix> bits are (mostly) freely selectable for addresses within
99+
// that network.
100+
//
101+
// Bitwise glossary:
102+
// - (1 << n) == 2**n
103+
// - `&` is bitwise and (setting all bits that are set in either operand)
104+
// - `~` is bitwise not (flipping all bits of the operand)
105+
// Jsonnet operator precedence: binary +- bind higher than shifts
106+
107+
// count is the number of available addresses (including the network and
108+
// broadcast address in the network). It's a value which has the
109+
// 32-<prefix> low bits set to 1 and all other bits set to 0.
110+
local count = (1 << 32 - prefix_length) - 1;
111+
// Netmask has the high <prefix> bits set to one and the 32-<prefix> low
112+
// bits set to 0. We can use `~count` as the mask to set the low
113+
// 32-<prefix> bits to 0, since count has only these bits set to 1 and
114+
// bitwise not flips all bits.
115+
local netmask = ((1 << 32) - 1) & ~count;
116+
// The network address is the first address in the network. By converting
117+
// the specified <addr> to an integer and using the netmask to set the low
118+
// 32-<prefix> bits to 0 we reliably get the network address regardless of
119+
// which IP in the network that the user specified for a given prefix.
120+
local net_addr = ipval(parts[0]) & netmask;
121+
122+
{
123+
network_address: format_ipval(net_addr),
124+
broadcast_address: format_ipval(net_addr + count),
125+
prefix_length: prefix_length,
126+
count: count,
127+
} + if prefix_length < 32 then {
128+
host_min: format_ipval(net_addr + 1),
129+
host_max: format_ipval(net_addr + count - 1),
130+
netmask: format_ipval(netmask),
131+
} else {};
132+
133+
{
134+
ipval: ipval,
135+
parse_ip_range: parse_ip_range,
136+
parse_cidr: parse_cidr,
137+
format_ipval: format_ipval,
138+
}

tests/golden/egress-gateway/cilium/cilium/40_egress_ip_managed_resource.yaml

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,12 @@ spec:
343343
find_egress_range: find_egress_range,
344344
}
345345
ipcalc.libsonnet: |
346+
// NOTE(sg): This file is symlinked to `component/espejote-templates` in
347+
// component-cilium to allow the `espejote-templates/egress-gateway.libsonnet`
348+
// library to work regardless of whether it's used by Espejote or the
349+
// component. We export this as a component library since it might be useful
350+
// for other components on Cilium-enabled clusters.
351+
346352
// Convert an IPv4 address in A.B.C.D format that's already been split into an
347353
// array to decimal format according to the formula `A*256^3 + B*256^2 + C*256
348354
// + D`. The decimal format allows us to make range comparisons and compute
@@ -355,11 +361,18 @@ spec:
355361
ip
356362
else
357363
std.split(ip, '.');
358-
std.foldl(
359-
function(v, p) v * 256 + p,
360-
std.map(std.parseInt, iparr),
361-
0
362-
);
364+
local iparr_int = std.map(std.parseInt, iparr);
365+
366+
if std.any(std.map(function(v) v > 255, iparr_int)) then
367+
error 'Error parsing IPv4 address: %s is not a valid address' % [
368+
ip,
369+
]
370+
else
371+
std.foldl(
372+
function(v, p) v * 256 + p,
373+
iparr_int,
374+
0
375+
);
363376
364377
// Extract start and end from the provided range, stripping any
365378
// whitespace. `prefix` is only used for the error message.
@@ -381,7 +394,7 @@ spec:
381394
382395
local format_ipval(val) =
383396
assert
384-
val >= 0 && val < ipval('255.255.255.255')
397+
val >= 0 && val <= ipval('255.255.255.255')
385398
: '%s not an IPv4 address in decimal' % val;
386399
387400
local iparr = std.reverse(std.foldl(
@@ -398,9 +411,74 @@ spec:
398411
399412
std.join('.', std.map(function(v) '%d' % v, iparr));
400413
414+
// Parse network in CIDR notation. Leading and trailing whitespace is
415+
// stripped. `prefix` is only used for the error message.
416+
//
417+
// This function correctly parses the full network info from arbitrary IPs in
418+
// CIDR notation. We return an object that's inspired by the output of the
419+
// Linux utility `ipcalc`.
420+
//
421+
// The return value contains the network address, broadcast address, count of
422+
// IPs in the CIDR and prefix length. For prefix lengths of less than 32, the
423+
// return value additionally contains the first and last host (in `min_host`
424+
// and `max_host`) and netmask.
425+
local parse_cidr(prefix, cidr) =
426+
local parts = std.split(std.stripChars(cidr, ' '), '/');
427+
if std.length(parts) != 2 then
428+
error 'Expected value for "%s" to be in CIDR notation, got "%s"' % [
429+
prefix,
430+
cidr,
431+
]
432+
else
433+
local prefix_length = std.parseInt(parts[1]);
434+
if prefix_length < 0 || prefix_length > 32 then
435+
error 'Invalid CIDR %s: prefix must be between 0 and 32' % cidr
436+
else
437+
// We compute count, netmask and network address using bitwise operations.
438+
// Jsonnet uses 64 bit integers for bitwise ops, so we don't have to worry
439+
// about overflowing when working with 32 bit values (IPv4 addresses).
440+
//
441+
// IPv4 CIDR notation works as follows: <addr>/<prefix> defines a network
442+
// where the first <prefix> bits of the IP are the "network" and the last
443+
// 32-<prefix> bits are (mostly) freely selectable for addresses within
444+
// that network.
445+
//
446+
// Bitwise glossary:
447+
// - (1 << n) == 2**n
448+
// - `&` is bitwise and (setting all bits that are set in either operand)
449+
// - `~` is bitwise not (flipping all bits of the operand)
450+
// Jsonnet operator precedence: binary +- bind higher than shifts
451+
452+
// count is the number of available addresses (including the network and
453+
// broadcast address in the network). It's a value which has the
454+
// 32-<prefix> low bits set to 1 and all other bits set to 0.
455+
local count = (1 << 32 - prefix_length) - 1;
456+
// Netmask has the high <prefix> bits set to one and the 32-<prefix> low
457+
// bits set to 0. We can use `~count` as the mask to set the low
458+
// 32-<prefix> bits to 0, since count has only these bits set to 1 and
459+
// bitwise not flips all bits.
460+
local netmask = ((1 << 32) - 1) & ~count;
461+
// The network address is the first address in the network. By converting
462+
// the specified <addr> to an integer and using the netmask to set the low
463+
// 32-<prefix> bits to 0 we reliably get the network address regardless of
464+
// which IP in the network that the user specified for a given prefix.
465+
local net_addr = ipval(parts[0]) & netmask;
466+
467+
{
468+
network_address: format_ipval(net_addr),
469+
broadcast_address: format_ipval(net_addr + count),
470+
prefix_length: prefix_length,
471+
count: count,
472+
} + if prefix_length < 32 then {
473+
host_min: format_ipval(net_addr + 1),
474+
host_max: format_ipval(net_addr + count - 1),
475+
netmask: format_ipval(netmask),
476+
} else {};
477+
401478
{
402479
ipval: ipval,
403480
parse_ip_range: parse_ip_range,
481+
parse_cidr: parse_cidr,
404482
format_ipval: format_ipval,
405483
}
406484
---

0 commit comments

Comments
 (0)