Skip to content

Commit 37b9635

Browse files
authored
Merge pull request #159 from projectsyn/feat/self-service-namespace-egress-ips
Add support for managing per-namespace egress IPs via namespace annotation
2 parents 0e28d83 + 0c549e9 commit 37b9635

13 files changed

+1376
-321
lines changed

class/defaults.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ parameters:
9595
policies: {}
9696
generate_shadow_ranges_configmap: false
9797
shadow_ranges_daemonset_node_selector: {}
98+
self_service_namespace_ips: false
9899
egress_ip_ranges: {}
99100

100101
l2_announcements:

component/egress-gateway-policies.jsonnet

Lines changed: 15 additions & 321 deletions
Original file line numberDiff line numberDiff line change
@@ -2,168 +2,34 @@ local com = import 'lib/commodore.libjsonnet';
22
local kap = import 'lib/kapitan.libjsonnet';
33
local kube = import 'lib/kube.libjsonnet';
44

5+
local egw = import 'espejote-templates/egress-gateway.libsonnet';
6+
57
local inv = kap.inventory();
68
local params = inv.parameters.cilium;
79

8-
local CiliumEgressGatewayPolicy(name) =
9-
kube._Object('cilium.io/v2', 'CiliumEgressGatewayPolicy', name) {
10-
metadata+: {
11-
annotations+: {
12-
'argocd.argoproj.io/sync-options': 'SkipDryRunOnMissingResource=true,Prune=false',
13-
},
14-
},
15-
};
16-
17-
local IsovalentEgressGatewayPolicy(name) =
18-
kube._Object('isovalent.com/v1', 'IsovalentEgressGatewayPolicy', name) {
19-
metadata+: {
20-
annotations+: {
21-
'argocd.argoproj.io/sync-options': 'SkipDryRunOnMissingResource=true',
22-
},
23-
},
24-
};
25-
2610
local EgressGatewayPolicy(name) =
2711
if params.release == 'enterprise' then
28-
IsovalentEgressGatewayPolicy(name)
12+
egw.IsovalentEgressGatewayPolicy(name)
2913
else
30-
CiliumEgressGatewayPolicy(name);
14+
egw.CiliumEgressGatewayPolicy(name);
3115

3216
local policies = com.generateResources(
3317
params.egress_gateway.policies,
3418
EgressGatewayPolicy
3519
);
3620

37-
// Convert an IPv4 address in A.B.C.D format that's already been split into an
38-
// array to decimal format according to the formula `A*256^3 + B*256^2 + C*256
39-
// + D`. The decimal format allows us to make range comparisons and compute
40-
// offsets into a range.
41-
// Parameter ip can either be the IP as a string, or already split into an
42-
// array holding each dotted part.
43-
local ipval(ip) =
44-
local iparr =
45-
if std.type(ip) == 'array' then
46-
ip
47-
else
48-
std.split(ip, '.');
49-
std.foldl(
50-
function(v, p) v * 256 + p,
51-
std.map(std.parseInt, iparr),
52-
0
53-
);
54-
55-
// Extract start and end from the provided range, stripping any
56-
// whitespace. `prefix` is only used for the error message.
57-
local parse_ip_range(prefix, rangespec) =
58-
local range_parts = std.map(
59-
function(s) std.stripChars(s, ' '),
60-
std.split(rangespec, '-')
61-
);
62-
if std.length(range_parts) != 2 then
63-
error 'Expected IP range for "%s" in format "192.0.2.32-192.0.2.63", got %s' % [
64-
prefix,
65-
rangespec,
66-
]
67-
else
68-
{
69-
start: range_parts[0],
70-
end: range_parts[1],
71-
};
72-
73-
74-
// Per-namespace egress IPs according to the selected design choice in
75-
// https://kb.vshn.ch/oc4/explanations/decisions/cloudscale-cilium-egressip.html
76-
// Requires that the shadow IPs are assigned to suitable dummy interfaces on
77-
// the hosts matching the node selector and that SNAT rules are in place to
78-
// map the shadow ranges to the public range.
79-
local NamespaceEgressPolicy =
80-
function(interface_prefix, egress_range, node_selector, egress_ip, namespace)
81-
// Helper which computes the interface index of the egress IP.
82-
// Assumes that the IPs in egress_range are assigned to dummy interfaces
83-
// named
84-
//
85-
// "<interface_prefix>_<i>"
86-
//
87-
// where i = 0..length(egress_range) - 1.
88-
local ifindex =
89-
local range = parse_ip_range(interface_prefix, egress_range);
90-
local start = ipval(range.start);
91-
local end = ipval(range.end);
92-
local ip = ipval(egress_ip);
93-
if start > end then
94-
error 'Egress IP range for "%s" is empty: %s > %s' % [
95-
interface_prefix,
96-
range.start,
97-
range.end,
98-
]
99-
else if start > ip || end < ip then
100-
error 'Egress IP for namespace "%s" (%s) outside of configured IP range (%s) for egress range "%s"' % [
101-
namespace,
102-
egress_ip,
103-
egress_range,
104-
interface_prefix,
105-
]
106-
else
107-
local idx = ip - start;
108-
local name = '%s_%d' % [ interface_prefix, idx ];
109-
if std.length(name) > 15 then
110-
error 'Interface name is longer than 15 characters: %s' % [ name ]
111-
else
112-
{
113-
value: idx,
114-
ifname: '%s_%d' % [ interface_prefix, idx ],
115-
debug: 'start=%d, end=%d, ip=%d' % [ start, end, ip ],
116-
};
117-
118-
EgressGatewayPolicy(namespace) {
119-
metadata+: {
120-
annotations+: {
121-
'cilium.syn.tools/description':
122-
'Generated policy to assign egress IP %s in egress range "%s" (%s) to namespace %s.' % [
123-
egress_ip,
124-
interface_prefix,
125-
egress_range,
126-
namespace,
127-
],
128-
'cilium.syn.tools/egress-ip': egress_ip,
129-
'cilium.syn.tools/interface-prefix': interface_prefix,
130-
'cilium.syn.tools/egress-range': egress_range,
131-
'cilium.syn.tools/source-namespace': namespace,
132-
'cilium.syn.tools/debug-interface-index': ifindex.debug,
133-
},
134-
},
135-
spec: {
136-
destinationCIDRs: [ '0.0.0.0/0' ],
137-
egressGroups: [
138-
{
139-
nodeSelector: {
140-
matchLabels: node_selector,
141-
},
142-
interface: ifindex.ifname,
143-
},
144-
],
145-
selectors: [
146-
{
147-
podSelector: {
148-
matchLabels: {
149-
'io.kubernetes.pod.namespace': namespace,
150-
},
151-
},
152-
},
153-
],
154-
},
155-
};
156-
15721
local egress_ip_policies = std.flattenArrays([
15822
local cfg = params.egress_gateway.egress_ip_ranges[interface_prefix];
15923
local ns_egress_ips = std.get(cfg, 'namespace_egress_ips', {});
16024
[
161-
NamespaceEgressPolicy(
25+
egw.NamespaceEgressPolicy(
16226
interface_prefix,
16327
cfg.egress_range,
28+
std.objectValues(std.get(cfg, 'shadow_ranges', [])),
16429
cfg.node_selector,
16530
ns_egress_ips[namespace],
16631
namespace,
32+
EgressGatewayPolicy,
16733
)
16834
for namespace in std.objectFields(ns_egress_ips)
16935
if ns_egress_ips[namespace] != null
@@ -172,184 +38,6 @@ local egress_ip_policies = std.flattenArrays([
17238
if params.egress_gateway.egress_ip_ranges[interface_prefix] != null
17339
]);
17440

175-
// NOTE(sg): This expects that each shadow range fully fits into a /24.
176-
local egress_ip_shadow_ranges =
177-
// Helper to extract the /24 prefix of the IP range passed as `range`. The
178-
// function raises an error if the provided range spans multiple /24.
179-
local extract_prefix(prefix, hostname, range) =
180-
// find <start_prefix>.0
181-
local start0 = ipval(std.mapWithIndex(
182-
function(idx, elem)
183-
if idx < 3 then elem else '0',
184-
std.split(range.start, '.'),
185-
));
186-
// find <end_prefix>.255
187-
local end255 = ipval(std.mapWithIndex(
188-
function(idx, elem)
189-
if idx < 3 then elem else '255',
190-
std.split(range.end, '.'),
191-
));
192-
if end255 - start0 + 1 > 256 then
193-
error "Shadow range %s-%s for '%s' in '%s' spans multiple /24. This isn't currently supported." % [
194-
range.start,
195-
range.end,
196-
hostname,
197-
prefix,
198-
]
199-
else
200-
// extract the /24 prefix from `range.start` now that we know that the
201-
// range fits into a single /24.
202-
std.join('.', std.split(range.start, '.')[0:3]);
203-
204-
local check_length(hostname, egress_range, range) =
205-
local public_range = parse_ip_range(
206-
egress_range.prefix,
207-
egress_range.config.egress_range
208-
);
209-
local public_len = ipval(public_range.end) - ipval(public_range.start);
210-
local shadow_len = ipval(range.end) - ipval(range.start);
211-
212-
if public_len != shadow_len then
213-
error "Shadow IP range %s-%s for '%s' in '%s' doesn't match length of egress IP range %s" % [
214-
range.start,
215-
range.end,
216-
hostname,
217-
egress_range.prefix,
218-
egress_range.config.egress_range,
219-
]
220-
else
221-
range;
222-
223-
224-
// Transform egress_ip_ranges.<range>.shadow_ranges into the format expected
225-
// by the systemd service (and script) managed in component
226-
// openshift4-nodes.
227-
local config = std.foldl(
228-
// Collect egress interface IP ranges by node. This object can be used to
229-
// generate the configmap that openshift4-nodes expects.
230-
function(data, egress_range)
231-
data {
232-
[hostname]+: {
233-
local range = check_length(
234-
hostname,
235-
egress_range,
236-
parse_ip_range(
237-
egress_range.prefix,
238-
egress_range.config.shadow_ranges[hostname]
239-
)
240-
),
241-
[egress_range.prefix]:
242-
{
243-
base: extract_prefix(egress_range.prefix, hostname, range),
244-
from: std.split(range.start, '.')[3],
245-
to: std.split(range.end, '.')[3],
246-
},
247-
}
248-
for hostname in std.objectFields(egress_range.config.shadow_ranges)
249-
},
250-
// transform egress_ip_ranges object into a list of key-value pair
251-
// objects, so we can more easily implement the transformation.
252-
[
253-
local data = params.egress_gateway.egress_ip_ranges[interface_prefix];
254-
{
255-
prefix: interface_prefix,
256-
config: data,
257-
}
258-
for interface_prefix in std.objectFields(params.egress_gateway.egress_ip_ranges)
259-
if params.egress_gateway.egress_ip_ranges[interface_prefix] != null
260-
&& std.objectHas(params.egress_gateway.egress_ip_ranges[interface_prefix], 'shadow_ranges')
261-
&& params.egress_gateway.egress_ip_ranges[interface_prefix].shadow_ranges != null
262-
],
263-
{}
264-
);
265-
266-
// generate 1 configmap for all egress ranges.
267-
local configmap =
268-
kube.ConfigMap('eip-shadow-ranges') {
269-
data: {
270-
[hostname]: std.manifestJsonMinified(config[hostname])
271-
for hostname in std.objectFields(config)
272-
},
273-
};
274-
275-
// Generate 1 daemonset per unique node selector across all configured
276-
// egress ranges. The daemonset's purpose is to make the configmap available
277-
// to the kubelet on the node, so that we can use the Kubelet kubeconfig for
278-
// the script managed by openshift4-nodes.
279-
local daemonset_configs = std.foldl(
280-
function(dses, d) dses + d,
281-
[
282-
local sel = params.egress_gateway.egress_ip_ranges[interface_prefix].node_selector;
283-
local sel_hash = std.md5(std.manifestJsonMinified(sel));
284-
{ [sel_hash]+: sel }
285-
for interface_prefix in std.objectFields(params.egress_gateway.egress_ip_ranges)
286-
if params.egress_gateway.egress_ip_ranges[interface_prefix] != null
287-
],
288-
{}
289-
);
290-
291-
local make_daemonset(ds_configs, sel_hash) =
292-
kube.DaemonSet(
293-
'eip-shadow-ranges-%s' % std.substr(
294-
sel_hash, std.length(sel_hash) - 5, 5
295-
)
296-
) {
297-
metadata+: {
298-
annotations+: {
299-
'cilium.syn.tools/description':
300-
'Daemonset which ensures that the Kubelet on the nodes where the'
301-
+ ' pods are scheduled can access configmap %s in namespace %s.' %
302-
[
303-
configmap.metadata.name,
304-
params._namespace,
305-
],
306-
},
307-
},
308-
spec+: {
309-
template+: {
310-
spec+: {
311-
containers_: {
312-
sleep: kube.Container('sleep') {
313-
image: '%(registry)s/%(image)s:%(tag)s' % params.images.kubectl,
314-
command: [ '/bin/sh', '-c', 'trap : TERM INT; sleep infinity & wait' ],
315-
volumeMounts_: {
316-
shadow_ranges: {
317-
mountPath: '/data/eip-shadow-ranges',
318-
},
319-
},
320-
},
321-
},
322-
nodeSelector: ds_configs[sel_hash],
323-
volumes_: {
324-
shadow_ranges: {
325-
configMap: {
326-
name: configmap.metadata.name,
327-
},
328-
},
329-
},
330-
},
331-
},
332-
},
333-
};
334-
335-
local daemonsets =
336-
if std.length(params.egress_gateway.shadow_ranges_daemonset_node_selector) == 0 then [
337-
make_daemonset(daemonset_configs, sel_hash)
338-
for sel_hash in std.objectFields(daemonset_configs)
339-
] else
340-
local sel_hash =
341-
std.md5(std.manifestJsonMinified(
342-
params.egress_gateway.shadow_ranges_daemonset_node_selector
343-
));
344-
[
345-
make_daemonset({
346-
[sel_hash]:
347-
params.egress_gateway.shadow_ranges_daemonset_node_selector,
348-
}, sel_hash),
349-
];
350-
351-
[ configmap ] + daemonsets;
352-
35341
// Check for duplicated source namespaces in the provided list of policies
35442
// Internal accumulator is an object which uses the source namespace as key
35543
// and contains the full policies as values. The function returns the values
@@ -372,13 +60,19 @@ local validate(policies) = std.objectValues(std.foldl(
37260
{}
37361
));
37462

63+
local shadow_ranges = import 'egress-gateway-shadow-ranges.libsonnet';
64+
local self_service = import 'egress-gateway-self-service.libsonnet';
65+
37566
{
37667
[if params.egress_gateway.enabled && std.length(params.egress_gateway.policies) > 0 then
37768
'20_egress_gateway_policies']: policies,
37869
[if params.egress_gateway.enabled && std.length(egress_ip_policies) > 0 then
37970
'20_namespace_egress_ip_policies']: validate(egress_ip_policies),
38071
[if params.egress_gateway.enabled &&
38172
params.egress_gateway.generate_shadow_ranges_configmap &&
382-
std.length(egress_ip_shadow_ranges) > 0 then
383-
'30_egress_ip_shadow_ranges']: egress_ip_shadow_ranges,
73+
std.length(shadow_ranges.manifests) > 0 then
74+
'30_egress_ip_shadow_ranges']: shadow_ranges.manifests,
75+
[if params.egress_gateway.enabled &&
76+
params.egress_gateway.self_service_namespace_ips then
77+
'40_egress_ip_managed_resource']: self_service.manifests,
38478
}

0 commit comments

Comments
 (0)