Skip to content

Commit 7de582f

Browse files
authored
Merge pull request #188 from projectsyn/feat/namespace-auto-egress-interfaces
Add support for automatically generating egress interface NMstate policies
2 parents 352400c + c3ea672 commit 7de582f

File tree

7 files changed

+288
-18
lines changed

7 files changed

+288
-18
lines changed

component/egress-gateway-policies.jsonnet

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ local kap = import 'lib/kapitan.libjsonnet';
33
local kube = import 'lib/kube.libjsonnet';
44

55
local egw = import 'espejote-templates/egress-gateway.libsonnet';
6+
local ipcalc = import 'lib/cilium-ipcalc.libsonnet';
7+
local nm = import 'lib/openshift-nmstate.libsonnet';
68

79
local inv = kap.inventory();
810
local params = inv.parameters.cilium;
911

12+
local has_nmstate = std.member(inv.applications, 'openshift-nmstate');
13+
1014
local EgressGatewayPolicy(name) =
1115
if params.release == 'enterprise' then
1216
egw.IsovalentEgressGatewayPolicy(name)
@@ -23,8 +27,22 @@ local egress_ip_policies = std.flattenArrays([
2327
local egress_range = egw.read_egress_range(interface_prefix, cfg);
2428
local ns_egress_ips = std.get(cfg, 'namespace_egress_ips', {});
2529
local dest_cidrs = com.renderArray(std.get(cfg, 'destination_cidrs', []));
26-
[
27-
egw.NamespaceEgressPolicy(
30+
local shadow_ranges = std.get(cfg, 'shadow_ranges', {});
31+
local auto_egress_interfaces =
32+
local requested = std.get(cfg, 'auto_egress_interfaces', false);
33+
local has_shadow_ranges = std.length(shadow_ranges) > 0;
34+
if requested && !has_nmstate then
35+
error
36+
'User requested auto egress interfaces for "%s", ' % interface_prefix +
37+
"but nmstate isn't present on the cluster."
38+
else if requested && !has_shadow_ranges then
39+
error
40+
'User requested auto egress interfaces for "%s", ' % interface_prefix +
41+
'but no shadow ranges are configured.'
42+
else
43+
requested && has_nmstate && has_shadow_ranges;
44+
std.flattenArrays([
45+
local ep = egw.NamespaceEgressPolicy(
2846
interface_prefix,
2947
'%(start)s - %(end)s' % egress_range,
3048
std.objectValues(std.get(cfg, 'shadow_ranges', {})),
@@ -35,10 +53,20 @@ local egress_ip_policies = std.flattenArrays([
3553
destination_cidrs=dest_cidrs,
3654
bgp_policy_labels=std.get(cfg, 'bgp_policy_labels', {}),
3755
policy_labels=std.get(cfg, 'policy_labels', {}),
38-
)
56+
);
57+
[ ep ] +
58+
if auto_egress_interfaces then
59+
egw.EgressInterfaceNNCPs(
60+
nm.NodeNetworkConfigurationPolicy,
61+
ep,
62+
interface_prefix,
63+
egress_range,
64+
shadow_ranges
65+
)
66+
else []
3967
for namespace in std.objectFields(ns_egress_ips)
4068
if ns_egress_ips[namespace] != null
41-
]
69+
])
4270
for interface_prefix in std.objectFields(params.egress_gateway.egress_ip_ranges)
4371
if params.egress_gateway.egress_ip_ranges[interface_prefix] != null
4472
]);
@@ -49,18 +77,23 @@ local egress_ip_policies = std.flattenArrays([
4977
// of this object.
5078
local validate(policies) = std.objectValues(std.foldl(
5179
function(seen, p)
52-
local ns =
53-
p.spec.selectors[0].podSelector.matchLabels['io.kubernetes.pod.namespace'];
54-
if std.objectHas(seen, ns) then
55-
error 'duplicated source namespace "%s" for policies in egress ranges "%s" and "%s"' % [
56-
ns,
57-
seen[ns].metadata.annotations['cilium.syn.tools/interface-prefix'],
58-
p.metadata.annotations['cilium.syn.tools/interface-prefix'],
59-
]
60-
else
80+
if p.kind != EgressGatewayPolicy('dummy').kind then
6181
seen {
62-
[ns]: p,
63-
},
82+
['%s-%s' % [ p.kind, p.metadata.name ]]: p,
83+
}
84+
else
85+
local ns =
86+
p.spec.selectors[0].podSelector.matchLabels['io.kubernetes.pod.namespace'];
87+
if std.objectHas(seen, ns) then
88+
error 'duplicated source namespace "%s" for policies in egress ranges "%s" and "%s"' % [
89+
ns,
90+
seen[ns].metadata.annotations['cilium.syn.tools/interface-prefix'],
91+
p.metadata.annotations['cilium.syn.tools/interface-prefix'],
92+
]
93+
else
94+
seen {
95+
[ns]: p,
96+
},
6497
policies,
6598
{}
6699
));

component/espejote-templates/egress-gateway-self-service.jsonnet

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ local reconcileNamespace(namespace) =
4343
// us) and update the namespace with an informational message.
4444
local range = res.range;
4545
local egress_range = egw.read_egress_range(range.if_prefix, range);
46-
[
46+
if std.get(range, 'auto_egress_interfaces', false) then [
47+
setAnnotations(namespace, {
48+
'cilium.syn.tools/egress-ip-status':
49+
"Allocating egress IP from range with `auto_egress_interfaces=true` isn't supported",
50+
}),
51+
] else [
4752
egw.NamespaceEgressPolicy(
4853
range.if_prefix,
4954
'%(start)s - %(end)s' % egress_range,

component/espejote-templates/egress-gateway.libsonnet

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,70 @@ local NamespaceEgressPolicy =
151151
},
152152
};
153153

154+
local EgressInterfaceNNCPs(NNCP, ep, interface_prefix, egress_range, shadow_ranges) = [
155+
local shadow_ip =
156+
local ifindex =
157+
local debuginfo = std.foldl(
158+
function(i, e)
159+
local parts = std.splitLimit(e, '=', 1);
160+
i { [parts[0]]: std.parseInt(parts[1]) },
161+
std.split(
162+
ep.metadata.annotations['cilium.syn.tools/debug-interface-index'],
163+
', '
164+
),
165+
{}
166+
);
167+
debuginfo.ip - debuginfo.start;
168+
local sr = ipcalc.parse_ip_range('shadow range for "%s" in "%s"' % [
169+
node,
170+
interface_prefix,
171+
], shadow_ranges[node]);
172+
ipcalc.format_ipval(ipcalc.ipval(sr.start) + ifindex);
173+
NNCP('egress-interface-%s-%s' % [
174+
ep.metadata.name,
175+
node,
176+
]) {
177+
metadata+: {
178+
annotations+: {
179+
'argocd.argoproj.io/sync-wave': '-10',
180+
'cilium.syn.tools/description':
181+
'Generated policy to configure egress interface "%s" for shadow range "%s" associated with egress range "%s" (%s) on node "%s".' % [
182+
ep.spec.egressGroups[0].interface,
183+
shadow_ranges[node],
184+
interface_prefix,
185+
'%(start)s - %(end)s' % egress_range,
186+
node,
187+
],
188+
},
189+
labels+: {
190+
'cilium.syn.tools/egress-policy': ep.metadata.name,
191+
},
192+
},
193+
spec: {
194+
nodeSelector: {
195+
'kubernetes.io/hostname': node,
196+
},
197+
desiredState: {
198+
interfaces: [
199+
{
200+
name: '%s' % ep.spec.egressGroups[0].interface,
201+
type: 'dummy',
202+
ipv4: {
203+
address: [ {
204+
ip: shadow_ip,
205+
'prefix-length': 32,
206+
} ],
207+
dhcp: false,
208+
enabled: true,
209+
},
210+
},
211+
],
212+
},
213+
},
214+
}
215+
for node in std.objectFields(shadow_ranges)
216+
];
217+
154218
local espejoteLabel = {
155219
'cilium.syn.tools/managed-by': 'espejote_cilium_namespace-egress-ips',
156220
};
@@ -230,6 +294,7 @@ local find_egress_range(ranges, egress_ip) =
230294
CiliumEgressGatewayPolicy: CiliumEgressGatewayPolicy,
231295
IsovalentEgressGatewayPolicy: IsovalentEgressGatewayPolicy,
232296
NamespaceEgressPolicy: NamespaceEgressPolicy,
297+
EgressInterfaceNNCPs: EgressInterfaceNNCPs,
233298
espejoteLabel: espejoteLabel,
234299
find_egress_range: find_egress_range,
235300
read_egress_range: read_egress_range,

docs/modules/ROOT/pages/references/parameters.adoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ default:: `{}`
381381
This parameter allows users to configure `CiliumEgressGatewayPolicy` (or `IsovalentEgressGatewayPolicy`) resources which assign a single egress IP to a namespace according to the design selected in https://kb.vshn.ch/oc4/explanations/decisions/cloudscale-cilium-egressip.html[Floating egress IPs with Cilium on cloudscale].
382382

383383
Each entry in the parameter is intended to describe a group of dummy interfaces that can be used in `CiliumEgressGatewayPolicy` (or `IsovalentEgressGatewayPolicy`) resources.
384-
The component expects that each value is an object with fields `egress_cidr`, `egress_range`, `node_selector`, `namespace_egress_ips`, `shadow_ranges`, `destination_cidrs`, `bgp_policy_labels`, and `policy_labels`.
384+
The component expects that each value is an object with fields `egress_cidr`, `egress_range`, `node_selector`, `namespace_egress_ips`, `shadow_ranges`, `destination_cidrs`, `bgp_policy_labels`, `policy_labels`, and `egress_auto_interfaces`.
385385
Fields `egress_cidr` and `egress_range` are mutually exclusive.
386386
The component raises an error for entries which set neither or both.
387387

@@ -436,6 +436,15 @@ The component adds all labels in `policy_labels` as labels on the generated egre
436436
If you specify labels in this parameter and in `bgp_policy_labels` and you specify the same label key in both fields, the label value provided in `bgp_policy_labels` will win.
437437
====
438438

439+
[NOTE]
440+
====
441+
Field `egress_auto_interfaces` is optional.
442+
443+
Setting the field to `true` raises an error for egress IP ranges which don't have any shadow ranges configured or if the `openshift-nmstate` component isn't present on the target cluster.
444+
445+
Otherwise, the component will generate a `NodeNetworkConfigurationPolicy` to create an egress interface with the correct shadow IP on each node for which a shadow range is configured.
446+
====
447+
439448
==== Prerequisites
440449

441450
The component expects that the key for each entry matches the prefix of the dummy interface names that are assigned the shadow IPs which map to the egress IP range defined in `egress_range`.

tests/egress-gateway.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Overwrite parameters here
2+
applications:
3+
- openshift-nmstate
24

35
parameters:
46
kapitan:
@@ -12,6 +14,9 @@ parameters:
1214
- type: https
1315
source: https://raw.githubusercontent.com/appuio/component-openshift4-monitoring/v6.11.3/lib/openshift4-monitoring-alert-patching.libsonnet
1416
output_path: vendor/lib/alert-patching.libsonnet
17+
- type: https
18+
source: https://raw.githubusercontent.com/appuio/component-openshift-nmstate/refs/heads/master/lib/openshift-nmstate.libsonnet
19+
output_path: vendor/lib/openshift-nmstate.libsonnet
1520
cilium:
1621
egress_gateway:
1722
enabled: true
@@ -57,6 +62,7 @@ parameters:
5762
namespace_egress_ips:
5863
baz: 192.0.2.93
5964
generate_shadow_ranges_configmap: false
65+
auto_egress_interfaces: true
6066
shadow_ranges:
6167
infra-8344: 198.51.100.96 - 198.51.100.127
6268
infra-87c9: 198.51.100.128 - 198.51.100.159

tests/golden/egress-gateway/cilium/cilium/20_namespace_egress_ip_policies.yaml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,84 @@
1+
apiVersion: nmstate.io/v1
2+
kind: NodeNetworkConfigurationPolicy
3+
metadata:
4+
annotations:
5+
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
6+
argocd.argoproj.io/sync-wave: '-10'
7+
cilium.syn.tools/description: Generated policy to configure egress interface "egress_c_29"
8+
for shadow range "198.51.100.96 - 198.51.100.127" associated with egress range
9+
"egress_c" (192.0.2.64 - 192.0.2.95) on node "infra-8344".
10+
labels:
11+
cilium.syn.tools/egress-policy: baz
12+
name: egress-interface-baz-infra-8344
13+
name: egress-interface-baz-infra-8344
14+
spec:
15+
desiredState:
16+
interfaces:
17+
- ipv4:
18+
address:
19+
- ip: 198.51.100.125
20+
prefix-length: 32
21+
dhcp: false
22+
enabled: true
23+
name: egress_c_29
24+
type: dummy
25+
nodeSelector:
26+
kubernetes.io/hostname: infra-8344
27+
---
28+
apiVersion: nmstate.io/v1
29+
kind: NodeNetworkConfigurationPolicy
30+
metadata:
31+
annotations:
32+
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
33+
argocd.argoproj.io/sync-wave: '-10'
34+
cilium.syn.tools/description: Generated policy to configure egress interface "egress_c_29"
35+
for shadow range "198.51.100.128 - 198.51.100.159" associated with egress range
36+
"egress_c" (192.0.2.64 - 192.0.2.95) on node "infra-87c9".
37+
labels:
38+
cilium.syn.tools/egress-policy: baz
39+
name: egress-interface-baz-infra-87c9
40+
name: egress-interface-baz-infra-87c9
41+
spec:
42+
desiredState:
43+
interfaces:
44+
- ipv4:
45+
address:
46+
- ip: 198.51.100.157
47+
prefix-length: 32
48+
dhcp: false
49+
enabled: true
50+
name: egress_c_29
51+
type: dummy
52+
nodeSelector:
53+
kubernetes.io/hostname: infra-87c9
54+
---
55+
apiVersion: nmstate.io/v1
56+
kind: NodeNetworkConfigurationPolicy
57+
metadata:
58+
annotations:
59+
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
60+
argocd.argoproj.io/sync-wave: '-10'
61+
cilium.syn.tools/description: Generated policy to configure egress interface "egress_c_29"
62+
for shadow range "198.51.100.160 - 198.51.100.191" associated with egress range
63+
"egress_c" (192.0.2.64 - 192.0.2.95) on node "infra-eba2".
64+
labels:
65+
cilium.syn.tools/egress-policy: baz
66+
name: egress-interface-baz-infra-eba2
67+
name: egress-interface-baz-infra-eba2
68+
spec:
69+
desiredState:
70+
interfaces:
71+
- ipv4:
72+
address:
73+
- ip: 198.51.100.189
74+
prefix-length: 32
75+
dhcp: false
76+
enabled: true
77+
name: egress_c_29
78+
type: dummy
79+
nodeSelector:
80+
kubernetes.io/hostname: infra-eba2
81+
---
182
apiVersion: cilium.io/v2
283
kind: CiliumEgressGatewayPolicy
384
metadata:

0 commit comments

Comments
 (0)