mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/kube-vip/kube-vip.git
synced 2026-09-21 00:23:59 +08:00
Compare commits
576 Commits
v0.7.1
...
error_warn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83d2092fec | ||
|
|
6c9c5af373 | ||
|
|
509eeea1d4 | ||
|
|
c00a61f45e | ||
|
|
0ea24655eb | ||
|
|
288cd9a8b0 | ||
|
|
2c00d2bc05 | ||
|
|
5cf899c88c | ||
|
|
16fa1bcc26 | ||
|
|
a67ef25c15 | ||
|
|
c82738633c | ||
|
|
11e419595b | ||
|
|
1db99a10dc | ||
|
|
f51f3276b5 | ||
|
|
199bc43c5c | ||
|
|
16afc9c1d4 | ||
|
|
76156b3f3b | ||
|
|
5a1e8c1a3f | ||
|
|
2d0f0734c4 | ||
|
|
42b97175e3 | ||
|
|
eec091af23 | ||
|
|
6f4f870800 | ||
|
|
3a5a59ae64 | ||
|
|
e8484fa1f3 | ||
|
|
55ccb8cd87 | ||
|
|
09edf341ab | ||
|
|
8380e4f07e | ||
|
|
4a07466467 | ||
|
|
cbdc86ac8f | ||
|
|
7ea39fa7b5 | ||
|
|
7eed2a33dc | ||
|
|
95bb7b9a85 | ||
|
|
2762fb624c | ||
|
|
3924a57168 | ||
|
|
8750b3331c | ||
|
|
be9415fef1 | ||
|
|
df13a69e26 | ||
|
|
6b60780d6c | ||
|
|
9786aa9446 | ||
|
|
71ca2614d2 | ||
|
|
3d171a937e | ||
|
|
2663a1b222 | ||
|
|
51ebcc40f0 | ||
|
|
95c45b0b32 | ||
|
|
a3c5be3242 | ||
|
|
c5c920f341 | ||
|
|
2e2951b35b | ||
|
|
4b741e767a | ||
|
|
66adbd4abb | ||
|
|
c9e4e7aea1 | ||
|
|
56a441f700 | ||
|
|
f9951bac77 | ||
|
|
d7a66ce20f | ||
|
|
73d9ce7f44 | ||
|
|
14ff1b9fec | ||
|
|
9a9c5998d8 | ||
|
|
edb9dcb626 | ||
|
|
89559b97af | ||
|
|
cc1d9ac16d | ||
|
|
1d9454c61b | ||
|
|
8409073e7a | ||
|
|
6d419f32bc | ||
|
|
9d68054e9a | ||
|
|
5dcfb8742f | ||
|
|
7e6f70b027 | ||
|
|
3e10aa85d0 | ||
|
|
16b9f6767e | ||
|
|
d8a9727ff5 | ||
|
|
016a899e60 | ||
|
|
68b39a83e0 | ||
|
|
93dabff000 | ||
|
|
630be48010 | ||
|
|
ea78d291ce | ||
|
|
2074be2939 | ||
|
|
7946c17c00 | ||
|
|
a0295d6a2f | ||
|
|
d70068b1a0 | ||
|
|
89baba07f5 | ||
|
|
95995500bc | ||
|
|
b1183e8a93 | ||
|
|
101f722110 | ||
|
|
3f390120c1 | ||
|
|
7baa8a3141 | ||
|
|
6435581674 | ||
|
|
7eb730c0ef | ||
|
|
f6a7aeb130 | ||
|
|
29296a9dc2 | ||
|
|
4d4a2f0ee1 | ||
|
|
e4e398bfcf | ||
|
|
8bd2c26a8f | ||
|
|
bd8f30d67d | ||
|
|
a3a429b2b9 | ||
|
|
4f7ce8a1c8 | ||
|
|
d2ecf22edd | ||
|
|
3963172e49 | ||
|
|
80c6b0bde4 | ||
|
|
4f59df38f3 | ||
|
|
1a3e6c9d5f | ||
|
|
0635ec9e01 | ||
|
|
3fff60a64a | ||
|
|
f05f0469cc | ||
|
|
00337a756b | ||
|
|
d3473b5d68 | ||
|
|
889d442288 | ||
|
|
b2c04c9058 | ||
|
|
0889ebed7d | ||
|
|
d1430e79e2 | ||
|
|
31eca367ab | ||
|
|
bdd353d0fd | ||
|
|
4deb0592f6 | ||
|
|
d8877072d4 | ||
|
|
89a8dc7de1 | ||
|
|
704c346f5e | ||
|
|
65061c5cd9 | ||
|
|
32233918b4 | ||
|
|
76169da60f | ||
|
|
9bcf1413f0 | ||
|
|
8e428e875f | ||
|
|
2fbecc25e5 | ||
|
|
02e77271d0 | ||
|
|
3d61888e58 | ||
|
|
efe75f491b | ||
|
|
000c139004 | ||
|
|
c39b84f0a9 | ||
|
|
ee958addaa | ||
|
|
8fe53351f8 | ||
|
|
bc9d860d83 | ||
|
|
332a23e543 | ||
|
|
b20713b50f | ||
|
|
94e96581ef | ||
|
|
61be6d0b6a | ||
|
|
be22805a7d | ||
|
|
25f6253286 | ||
|
|
f3e9fb6ea9 | ||
|
|
0f3dda02c4 | ||
|
|
a2873b5465 | ||
|
|
202d45e5ab | ||
|
|
f5e4612c03 | ||
|
|
de888c501c | ||
|
|
47bc83c248 | ||
|
|
1cf637c569 | ||
|
|
ae2571e241 | ||
|
|
86f5e9b8b2 | ||
|
|
ce61ff085a | ||
|
|
b816e154cf | ||
|
|
ac1238c337 | ||
|
|
10dbf2c0ef | ||
|
|
cf68f8639c | ||
|
|
b114c11b0f | ||
|
|
42b7a8152b | ||
|
|
f7821c7fb3 | ||
|
|
42478905d0 | ||
|
|
8d55bd3b63 | ||
|
|
16247fc3a3 | ||
|
|
b56b80cd30 | ||
|
|
ba25e0e583 | ||
|
|
8f1fe355fc | ||
|
|
649d9bf0ef | ||
|
|
a5108a69aa | ||
|
|
c74a496299 | ||
|
|
b74c274466 | ||
|
|
56b3867e57 | ||
|
|
66d237bfbc | ||
|
|
68071b214e | ||
|
|
a82ca5576b | ||
|
|
81dd386b4e | ||
|
|
b61a74396d | ||
|
|
105fbc522a | ||
|
|
2b52c39242 | ||
|
|
25d39bca09 | ||
|
|
644226321e | ||
|
|
3928dda541 | ||
|
|
d9a7f413a6 | ||
|
|
eb300bb634 | ||
|
|
c30fd9e7be | ||
|
|
2fc969b848 | ||
|
|
47884088ec | ||
|
|
70e1212396 | ||
|
|
d947c2abcc | ||
|
|
e2efb64aea | ||
|
|
01279d45e3 | ||
|
|
f2a7cad218 | ||
|
|
98163341d3 | ||
|
|
a71d361d15 | ||
|
|
cf24ad835d | ||
|
|
0c04088b16 | ||
|
|
22489ad095 | ||
|
|
1fa3da45fa | ||
|
|
d497df3767 | ||
|
|
bdd3c5c191 | ||
|
|
5b41db2246 | ||
|
|
1eb35774a5 | ||
|
|
7d7036fae9 | ||
|
|
1a4bf13819 | ||
|
|
3e225bf51f | ||
|
|
d6837cbe7d | ||
|
|
40994e0464 | ||
|
|
4e18ad189d | ||
|
|
a0ed07913e | ||
|
|
3d9ca62031 | ||
|
|
e0f4520437 | ||
|
|
3a5ebd184d | ||
|
|
a7d19c15f1 | ||
|
|
9822d92bed | ||
|
|
4d8b7750ae | ||
|
|
3bcf783020 | ||
|
|
1b3a7bb5de | ||
|
|
0d5ac98209 | ||
|
|
e2472e509b | ||
|
|
08388496bc | ||
|
|
f4eab023e8 | ||
|
|
c5e854f323 | ||
|
|
e7b9439161 | ||
|
|
555ca2b830 | ||
|
|
271f21f203 | ||
|
|
de54fcbd11 | ||
|
|
46cad395e1 | ||
|
|
3466947f69 | ||
|
|
8696f80525 | ||
|
|
51527d3e6c | ||
|
|
c8a9189bf6 | ||
|
|
896b0983e4 | ||
|
|
d55b124251 | ||
|
|
72b6e22d90 | ||
|
|
3272bc1f8b | ||
|
|
1e754703e8 | ||
|
|
119424bb9b | ||
|
|
d8926ebea5 | ||
|
|
6b1a0a7ea8 | ||
|
|
c92d01b957 | ||
|
|
d6c6d8e529 | ||
|
|
1d086b2d5c | ||
|
|
e5d967dcf4 | ||
|
|
21a5533936 | ||
|
|
d21fec6c7d | ||
|
|
9c4ae86937 | ||
|
|
9c67660b94 | ||
|
|
96d020b6ad | ||
|
|
bc42c3e2c3 | ||
|
|
ea45dafcf3 | ||
|
|
3e3fd21c16 | ||
|
|
70b83664a9 | ||
|
|
9650a00157 | ||
|
|
f851ddbcaf | ||
|
|
a5ee4d969b | ||
|
|
2dfd46decb | ||
|
|
6752dc0fdd | ||
|
|
c7174d3c94 | ||
|
|
d33ba7e22e | ||
|
|
9b552a4d29 | ||
|
|
5fc04a24f8 | ||
|
|
122b18a81d | ||
|
|
c9bc005abe | ||
|
|
ee0f0668ab | ||
|
|
826bb6fc8a | ||
|
|
b9aa99a208 | ||
|
|
2b0aa825b5 | ||
|
|
24b1524aee | ||
|
|
cf7c7f2f25 | ||
|
|
c082688e67 | ||
|
|
18bfe6a8d2 | ||
|
|
c367434798 | ||
|
|
f1ca914e92 | ||
|
|
2763c7e417 | ||
|
|
f9f0004d3f | ||
|
|
958eaefb40 | ||
|
|
3752695000 | ||
|
|
d6eb7c4f26 | ||
|
|
b88769e109 | ||
|
|
eddbcf0801 | ||
|
|
886f183108 | ||
|
|
aba1d53699 | ||
|
|
d05530e030 | ||
|
|
7d52dbbbd5 | ||
|
|
53185bd58d | ||
|
|
8997c4b1a8 | ||
|
|
ce4665bbec | ||
|
|
e1ecca42c5 | ||
|
|
c6cb548763 | ||
|
|
b52c90f865 | ||
|
|
2ceba7f5ad | ||
|
|
c3121a97f1 | ||
|
|
b7cc63bfd6 | ||
|
|
3a4d859457 | ||
|
|
964b248108 | ||
|
|
b6e87418a2 | ||
|
|
473fd6f4f8 | ||
|
|
c49afdb44d | ||
|
|
f0182fcb0c | ||
|
|
7424df98a5 | ||
|
|
b4c4cfd365 | ||
|
|
7878be3847 | ||
|
|
1dc75f9f21 | ||
|
|
cbe4cd150e | ||
|
|
99e1fb3fc6 | ||
|
|
e1481db95c | ||
|
|
efc7ba5646 | ||
|
|
90552b73dc | ||
|
|
81c0b4faf5 | ||
|
|
95be514b8f | ||
|
|
d289efc862 | ||
|
|
e388b2d1c9 | ||
|
|
7ca0d79250 | ||
|
|
20b375770c | ||
|
|
6b3ada8402 | ||
|
|
c12bb65b4b | ||
|
|
2e0dddba60 | ||
|
|
8e6767c606 | ||
|
|
419051c762 | ||
|
|
9e432d1178 | ||
|
|
0832a7227c | ||
|
|
3b16caa38c | ||
|
|
1d4e57a754 | ||
|
|
c36585ce98 | ||
|
|
af1280be1a | ||
|
|
c01fb8fca7 | ||
|
|
1d6d860153 | ||
|
|
2136e69a82 | ||
|
|
cb438d289b | ||
|
|
ded08ddf5c | ||
|
|
79b24875ec | ||
|
|
b423cbdb89 | ||
|
|
600c1db24b | ||
|
|
b2979be25c | ||
|
|
462b511b9f | ||
|
|
19e660d4a6 | ||
|
|
cfa11d1a88 | ||
|
|
bf283f1252 | ||
|
|
839b860eee | ||
|
|
e134e5682e | ||
|
|
ebc4cdccb9 | ||
|
|
baed70dbfc | ||
|
|
b8aff1d348 | ||
|
|
aa5f0cc267 | ||
|
|
316c3bbdd4 | ||
|
|
dbc02485d9 | ||
|
|
6442ce26fc | ||
|
|
ccd1137606 | ||
|
|
96d79774b5 | ||
|
|
829a1fced6 | ||
|
|
0c5dd3c890 | ||
|
|
51aad755c3 | ||
|
|
635950e329 | ||
|
|
7eb93bec0b | ||
|
|
cf8384fb1f | ||
|
|
6da951056c | ||
|
|
a50e476d7e | ||
|
|
329e0940da | ||
|
|
7a43646e62 | ||
|
|
dd8f2d13d2 | ||
|
|
f119c890a1 | ||
|
|
714b80225f | ||
|
|
49297980b0 | ||
|
|
e7beb9c9c3 | ||
|
|
5e66a62c32 | ||
|
|
fbbf83d9d0 | ||
|
|
7e244084e1 | ||
|
|
089ad123b6 | ||
|
|
fdc50efc86 | ||
|
|
7644cb720e | ||
|
|
299c9e1ebb | ||
|
|
b82b733dde | ||
|
|
e339d12b40 | ||
|
|
3ea17b134d | ||
|
|
ea410e7490 | ||
|
|
430efae598 | ||
|
|
c96cdb6cd2 | ||
|
|
ffe2e9c808 | ||
|
|
545f9a4a47 | ||
|
|
2555dd8101 | ||
|
|
f7b4ab5b42 | ||
|
|
4c70d87381 | ||
|
|
894e56458f | ||
|
|
de5659e7b2 | ||
|
|
e4b0b2a71f | ||
|
|
dd06c3fb82 | ||
|
|
8ee952cf3e | ||
|
|
57bcea9646 | ||
|
|
907696a4a3 | ||
|
|
78cdf8d5b6 | ||
|
|
5a7edbe825 | ||
|
|
de8300a40a | ||
|
|
73e6ade0ea | ||
|
|
4def2c784e | ||
|
|
1c8ce223d7 | ||
|
|
02d00bf99c | ||
|
|
4433243fa6 | ||
|
|
a8ae976bb2 | ||
|
|
1559b21cfb | ||
|
|
f55aed4382 | ||
|
|
cc12fcb0f6 | ||
|
|
95dfc47e47 | ||
|
|
0bf38f57f7 | ||
|
|
7f4116417c | ||
|
|
5763400c15 | ||
|
|
6b136efff3 | ||
|
|
1f089fe71c | ||
|
|
1bc26b5827 | ||
|
|
40674a12b6 | ||
|
|
ef2f6ebaa0 | ||
|
|
625030dfb6 | ||
|
|
779fc5641f | ||
|
|
971e794639 | ||
|
|
89c6002f92 | ||
|
|
6b0e45da96 | ||
|
|
7bde7a4845 | ||
|
|
eda0bd69b3 | ||
|
|
2a56bdb0c8 | ||
|
|
de0375610d | ||
|
|
382024104a | ||
|
|
e896b55c25 | ||
|
|
53ce87755a | ||
|
|
750f78164f | ||
|
|
bf6cb8e39e | ||
|
|
f58e110057 | ||
|
|
98db46c817 | ||
|
|
da9b440f1e | ||
|
|
a18039373d | ||
|
|
a9753fafd2 | ||
|
|
8aa0a6b9e0 | ||
|
|
8f16021e4c | ||
|
|
456d52d507 | ||
|
|
4da24fb64f | ||
|
|
c27044b521 | ||
|
|
810cf89f97 | ||
|
|
d65af2c84f | ||
|
|
d30fda1d9d | ||
|
|
b6d4aa1632 | ||
|
|
b024d04d91 | ||
|
|
6cdc7a86c4 | ||
|
|
c6f1c740fd | ||
|
|
95c43472e9 | ||
|
|
c8a222e244 | ||
|
|
37abb44a64 | ||
|
|
b4e6725a1a | ||
|
|
b6410d9b4d | ||
|
|
4bdfeffc96 | ||
|
|
a32804dfee | ||
|
|
b24ce18d55 | ||
|
|
5512a08aac | ||
|
|
f438367d6e | ||
|
|
62a724122c | ||
|
|
fb86cb8687 | ||
|
|
29a0dc6c07 | ||
|
|
82f3c6a491 | ||
|
|
fbb2746d07 | ||
|
|
185d8bc484 | ||
|
|
db88185c40 | ||
|
|
611cb0288f | ||
|
|
b18bdd0fb9 | ||
|
|
8ef0d459c0 | ||
|
|
41bc78bc8f | ||
|
|
05984b9860 | ||
|
|
82b1e8536b | ||
|
|
425cb92dee | ||
|
|
9470edbea9 | ||
|
|
a5d6846608 | ||
|
|
04ce471366 | ||
|
|
6c090701ea | ||
|
|
87fd49ac98 | ||
|
|
a0a19ea3fa | ||
|
|
a571d0a9c2 | ||
|
|
6f267462bb | ||
|
|
a52e9a1818 | ||
|
|
e883c1ea5d | ||
|
|
59951cbd4b | ||
|
|
9abce4a215 | ||
|
|
0b33aa64fc | ||
|
|
57980bd23f | ||
|
|
69f4c389c5 | ||
|
|
f56c9b7480 | ||
|
|
f8af9c37af | ||
|
|
c16f634cf7 | ||
|
|
5df534a1e7 | ||
|
|
ee535938b4 | ||
|
|
0df8a0dff4 | ||
|
|
0e2a10535b | ||
|
|
b5d9ede6f8 | ||
|
|
94bad6d53b | ||
|
|
927bb95ead | ||
|
|
7b96ebc7d4 | ||
|
|
5b05f365f1 | ||
|
|
cd3d8a592c | ||
|
|
e69fadf19b | ||
|
|
5ded2d7574 | ||
|
|
c683bb879e | ||
|
|
392583d460 | ||
|
|
abf49f2fa6 | ||
|
|
25c2532e74 | ||
|
|
28918e839f | ||
|
|
1620e4c890 | ||
|
|
c8bdf61a87 | ||
|
|
e9835aa981 | ||
|
|
ca3dad817d | ||
|
|
cb29d7cb17 | ||
|
|
0fa7cbdab8 | ||
|
|
0038c27fec | ||
|
|
4c19a3370c | ||
|
|
25abee70cd | ||
|
|
057a32d191 | ||
|
|
b33a7b6d32 | ||
|
|
ba0c9282e7 | ||
|
|
a431daab42 | ||
|
|
54e4334b9f | ||
|
|
6545a5aaab | ||
|
|
2872256a89 | ||
|
|
a1ae304d07 | ||
|
|
c90d2043f0 | ||
|
|
722c47fc08 | ||
|
|
27fae664fe | ||
|
|
7349576e5b | ||
|
|
1ab5047941 | ||
|
|
950264e403 | ||
|
|
0686b92376 | ||
|
|
b7ebb21dc0 | ||
|
|
ab904e2e18 | ||
|
|
a5d4d0ca98 | ||
|
|
7fb301d3b3 | ||
|
|
aa4da2f72a | ||
|
|
5dc91e6b3b | ||
|
|
658ca8697e | ||
|
|
979c016d98 | ||
|
|
8e4abea78e | ||
|
|
3caedde7fe | ||
|
|
fb43a48a87 | ||
|
|
12e298bb22 | ||
|
|
f514269408 | ||
|
|
2c5d366487 | ||
|
|
fea05fa3ad | ||
|
|
5c515f0c0d | ||
|
|
b0acf844ca | ||
|
|
bd3df6b616 | ||
|
|
5100cd0e9a | ||
|
|
04d863310b | ||
|
|
29713acfeb | ||
|
|
8d607b89be | ||
|
|
877ee75e75 | ||
|
|
d3da4d934a | ||
|
|
c3e9a13f1c | ||
|
|
59ad5ec9a2 | ||
|
|
8a80a72d94 | ||
|
|
cb1e63c302 | ||
|
|
e87ef6b3b8 | ||
|
|
f87665c67f | ||
|
|
c9d11e7123 | ||
|
|
51512201ea | ||
|
|
e37091c900 | ||
|
|
b484be799e | ||
|
|
2134c6b1fe | ||
|
|
fbc6904fa0 | ||
|
|
0cedf6a97d | ||
|
|
932fe09870 | ||
|
|
f1cf044eae | ||
|
|
a10a478f24 | ||
|
|
d1541c3464 | ||
|
|
4e3717f089 | ||
|
|
2aa799c9d0 | ||
|
|
bb08489898 | ||
|
|
1ea277f43e | ||
|
|
2c4ff6949a | ||
|
|
1a4bd01332 | ||
|
|
9e2a13c133 | ||
|
|
39fbbc57ac | ||
|
|
b9f8c3b0f5 | ||
|
|
ac5895a311 | ||
|
|
e7eaa70fd4 | ||
|
|
76cf59c7a7 | ||
|
|
a05ecbccb7 | ||
|
|
c531414542 | ||
|
|
70b4728492 | ||
|
|
835007b07a | ||
|
|
6d0c132519 | ||
|
|
485cf6a0dd | ||
|
|
1338e4fcd3 | ||
|
|
981355d910 | ||
|
|
e89d6f9f00 |
4
.github/workflows/anchore-syft.yml
vendored
4
.github/workflows/anchore-syft.yml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
- name: Anchore SBOM Action
|
||||
uses: anchore/sbom-action@v0.15.8
|
||||
uses: anchore/sbom-action@v0.20.10
|
||||
with:
|
||||
format: cyclonedx-json
|
||||
|
||||
71
.github/workflows/ci-pull-request.yaml
vendored
Normal file
71
.github/workflows/ci-pull-request.yaml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: For each PR
|
||||
on:
|
||||
pull_request:
|
||||
jobs:
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E tests
|
||||
strategy:
|
||||
matrix:
|
||||
mode: ["arp", "rt", "bgp"]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d-%H-%M')"
|
||||
- name: Ensure fs wont cause issues
|
||||
run: sudo sysctl fs.inotify.max_user_instances=8192 && sudo sysctl fs.inotify.max_user_watches=524288
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build image locally
|
||||
run: make dockerx86Local
|
||||
- name: Run Manifest generation tests
|
||||
run: make manifest-test
|
||||
- name: Run ARP mode tests v1.29.0 onwards
|
||||
run: E2E_KEEP_LOGS=true make e2e-tests129-arp
|
||||
if: matrix.mode== 'arp'
|
||||
- name: Run RT mode tests v1.29.0 onwards
|
||||
run: E2E_KEEP_LOGS=true make e2e-tests129-rt
|
||||
if: matrix.mode== 'rt'
|
||||
- name: Get GoBGP binaries
|
||||
run: make get-gobgp
|
||||
if: matrix.mode== 'bgp'
|
||||
- name: Run BGP mode tests v1.29.0 onwards
|
||||
run: sudo -E PATH=$PATH DOCKER_API_VERSION=1.48 E2E_KEEP_LOGS=true make e2e-tests129-bgp
|
||||
if: matrix.mode== 'bgp'
|
||||
- name: Change log directory permissions
|
||||
run: sudo chmod -R 755 /tmp/kube-vip-test-${{ matrix.mode }}*
|
||||
if: matrix.mode== 'bgp' && always()
|
||||
- name: Save logs
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: e2e-test-logs-${{ matrix.mode }}-${{ steps.date.outputs.date }}
|
||||
path: /tmp/kube-vip-test-${{ matrix.mode }}*
|
||||
if: always()
|
||||
service-e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E service tests
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d-%H-%M')"
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build image with iptables
|
||||
run: make dockerx86ActionIPTables
|
||||
- name: Run tests
|
||||
run: DOCKERTAG=action E2E_KEEP_LOGS=true make service-tests
|
||||
- name: Save logs
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: services-test-logs-${{ steps.date.outputs.date }}
|
||||
path: /tmp/kube-vip-service-tests*
|
||||
if: always()
|
||||
67
.github/workflows/ci.yaml
vendored
67
.github/workflows/ci.yaml
vendored
@@ -1,22 +1,20 @@
|
||||
name: For each commit and PR
|
||||
name: For each commit
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
env:
|
||||
GO_VERSION: "1.21"
|
||||
jobs:
|
||||
validation:
|
||||
runs-on: ubuntu-latest
|
||||
name: Checks and linters
|
||||
steps:
|
||||
- name: Init
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential golint && sudo sysctl fs.inotify.max_user_instances=8192 && sudo sysctl fs.inotify.max_user_watches=524288
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential && sudo sysctl fs.inotify.max_user_instances=8192 && sudo sysctl fs.inotify.max_user_watches=524288
|
||||
- name: Install golangci-lint
|
||||
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
|
||||
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.8
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: All checks
|
||||
@@ -26,9 +24,9 @@ jobs:
|
||||
name: Unit tests
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Run tests
|
||||
@@ -37,61 +35,28 @@ jobs:
|
||||
name: Integration tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Run tests
|
||||
run: make integration-tests
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E ARP tests
|
||||
steps:
|
||||
- name: Ensure fs wont cause issues
|
||||
run: sudo sysctl fs.inotify.max_user_instances=8192 && sudo sysctl fs.inotify.max_user_watches=524288
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build image locally
|
||||
run: make dockerx86Local
|
||||
- name: Run Control plane tests
|
||||
run: make e2e-tests
|
||||
- name: Run Control plane tests v1.29.0 onwards
|
||||
run: make e2e-tests129
|
||||
service-e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E service tests
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build image with iptables
|
||||
run: make dockerx86ActionIPTables
|
||||
- name: Run tests
|
||||
run: DOCKERTAG=action make service-tests
|
||||
run: make integration-tests
|
||||
image-vul-check:
|
||||
runs-on: ubuntu-latest
|
||||
name: Image vulnerability scan
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Build image with iptables
|
||||
run: make dockerx86ActionIPTables
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
image-ref: 'plndr/kube-vip:action'
|
||||
format: 'table'
|
||||
exit-code: '1'
|
||||
image-ref: "plndr/kube-vip:action"
|
||||
format: "table"
|
||||
exit-code: "1"
|
||||
ignore-unfixed: true
|
||||
vuln-type: 'os,library'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
vuln-type: "os,library"
|
||||
severity: "CRITICAL,HIGH"
|
||||
|
||||
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
@@ -38,16 +38,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
6
.github/workflows/main.yaml
vendored
6
.github/workflows/main.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build standard version
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
ghcr.io/kube-vip/kube-vip:${{ github.ref_name }}
|
||||
- name: Build iptables version
|
||||
id: docker_build_iptables
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile_iptables
|
||||
|
||||
14
.github/workflows/release.yaml
vendored
14
.github/workflows/release.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
@@ -27,13 +27,20 @@ jobs:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate Metadata
|
||||
uses: docker/metadata-action@v5.9.0
|
||||
id: metadata
|
||||
with:
|
||||
labels: |
|
||||
org.opencontainers.image.documentation=https://kube-vip.io/docs/
|
||||
- name: Build and push main branch
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
tags: >-
|
||||
plndr/kube-vip:${{ github.ref_name }},
|
||||
plndr/kube-vip:latest,
|
||||
@@ -41,12 +48,13 @@ jobs:
|
||||
ghcr.io/kube-vip/kube-vip:latest
|
||||
- name: Build iptables version and push main branch
|
||||
id: docker_build_iptables
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile_iptables
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
tags: >-
|
||||
plndr/kube-vip-iptables:${{ github.ref_name }},
|
||||
plndr/kube-vip-iptables:latest,
|
||||
|
||||
33
CHANGELOG.md
Normal file
33
CHANGELOG.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- SIGUSR1 signal handler for runtime configuration dumps (#1301)
|
||||
- Send SIGUSR1 to kube-vip process to dump current configuration to stdout
|
||||
- Configuration dump includes:
|
||||
- Basic configuration (VIP, interface, port, namespace settings)
|
||||
- BGP configuration (enabled status, AS number, router ID, peers)
|
||||
- ARP/NDP configuration (enabled status, broadcast rate)
|
||||
- Services configuration (enabled status, load balancer settings)
|
||||
- Network interfaces status
|
||||
- Leader election configuration (type, lease details)
|
||||
- Runtime statistics (load balancer, Prometheus, health check settings)
|
||||
- Output format: Human-readable plaintext via fmt.Printf()
|
||||
- Thread-safe implementation using mutex protection
|
||||
- Non-disruptive: Process continues running after configuration dump
|
||||
- Added comprehensive unit tests for all dump methods
|
||||
- Added E2E tests for signal handling
|
||||
|
||||
### Changed
|
||||
- Updated signal handlers in manager_arp.go, manager_bgp.go, manager_wireguard.go, and manager_table.go to use switch statement pattern for handling multiple signals (SIGUSR1, SIGINT, SIGTERM)
|
||||
|
||||
## [v1.0.1] - Previous Release
|
||||
|
||||
### Previous changes
|
||||
- See git history for changes prior to CHANGELOG.md introduction
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:experimental
|
||||
|
||||
FROM golang:1.22.0-alpine3.18 as dev
|
||||
FROM golang:1.25.4-alpine3.22 as dev
|
||||
RUN apk add --no-cache git ca-certificates make
|
||||
RUN adduser -D appuser
|
||||
COPY . /src/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:experimental
|
||||
|
||||
FROM golang:1.22.0-alpine3.18 as dev
|
||||
FROM golang:1.25.4-alpine3.22 as dev
|
||||
RUN apk add --no-cache git make
|
||||
RUN adduser -D appuser
|
||||
COPY . /src/
|
||||
@@ -11,10 +11,10 @@ RUN --mount=type=cache,sharing=locked,id=gomod,target=/go/pkg/mod/cache \
|
||||
--mount=type=cache,sharing=locked,id=goroot,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux make build
|
||||
|
||||
FROM alpine:3.19.1
|
||||
FROM alpine:3.22.2
|
||||
# Update pkgs and add iptables
|
||||
RUN apk upgrade && \
|
||||
apk add --no-cache iptables
|
||||
apk add --no-cache iptables iptables-legacy
|
||||
|
||||
# Add kube-vip binary
|
||||
COPY --from=dev /src/kube-vip /
|
||||
|
||||
74
Makefile
74
Makefile
@@ -5,7 +5,7 @@ TARGET := kube-vip
|
||||
.DEFAULT_GOAL := $(TARGET)
|
||||
|
||||
# These will be provided to the target
|
||||
VERSION := v0.7.1
|
||||
VERSION := v1.0.2
|
||||
|
||||
BUILD := `git rev-parse HEAD`
|
||||
|
||||
@@ -15,9 +15,9 @@ TARGETOS=linux
|
||||
# Use linker flags to provide version/build settings to the target
|
||||
LDFLAGS=-ldflags "-s -w -X=main.Version=$(VERSION) -X=main.Build=$(BUILD) -extldflags -static"
|
||||
DOCKERTAG ?= $(VERSION)
|
||||
REPOSITORY ?= plndr
|
||||
REPOSITORY ?= docker.io/plndr
|
||||
|
||||
.PHONY: all build clean install uninstall fmt simplify check run e2e-tests
|
||||
.PHONY: all build clean install uninstall simplify check run e2e-tests
|
||||
|
||||
all: check install
|
||||
|
||||
@@ -37,9 +37,6 @@ install:
|
||||
uninstall: clean
|
||||
@rm -f $$(which ${TARGET})
|
||||
|
||||
fmt:
|
||||
@gofmt -l -w ./...
|
||||
|
||||
demo:
|
||||
@cd demo
|
||||
@docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x --push -t $(REPOSITORY)/$(TARGET):$(DOCKERTAG) .
|
||||
@@ -59,6 +56,11 @@ dockerx86Iptables:
|
||||
@docker buildx build --platform linux/amd64 -f ./Dockerfile_iptables --push -t $(REPOSITORY)/$(TARGET):dev .
|
||||
@echo New single x86 Architecture Docker image created
|
||||
|
||||
dockerx86IptablesLocal:
|
||||
@-rm ./kube-vip
|
||||
@docker buildx build --platform linux/amd64 -f ./Dockerfile_iptables -t $(REPOSITORY)/$(TARGET):$(DOCKERTAG) .
|
||||
@echo New single x86 Architecture Docker image created
|
||||
|
||||
dockerx86:
|
||||
@-rm ./kube-vip
|
||||
@docker buildx build --platform linux/amd64 --push -t $(REPOSITORY)/$(TARGET):$(DOCKERTAG) .
|
||||
@@ -92,12 +94,12 @@ dockerLocal:
|
||||
@echo New Multi Architecture Docker image created
|
||||
|
||||
simplify:
|
||||
@gofmt -s -l -w ./...
|
||||
@gofmt -s -l -w *.go pkg cmd
|
||||
|
||||
check:
|
||||
go mod tidy
|
||||
test -z "$(git status --porcelain)"
|
||||
test -z $(shell gofmt -l main.go | tee /dev/stderr) || echo "[WARN] Fix formatting issues with 'make fmt'"
|
||||
test -z $(shell gofmt -l *.go pkg cmd) || echo "[WARN] Fix formatting issues with 'make simplify'"
|
||||
golangci-lint run
|
||||
go vet ./...
|
||||
|
||||
@@ -107,15 +109,23 @@ run: install
|
||||
manifests:
|
||||
@make build
|
||||
@mkdir -p ./docs/manifests/$(VERSION)/
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-arp.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-lb.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --bgp --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-bgp.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-arp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --inCluster --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-ds-lb.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --bgp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-bgp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --bgp --leaderElection --controlplane --services --inCluster --provider-config /etc/cloud-sa/cloud-sa.json > ./docs/manifests/$(VERSION)/kube-vip-bgp-em-ds.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-arp.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-lb.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-bgp.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-arp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-ds-lb.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-bgp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-bgp-em-ds.yaml
|
||||
@-rm ./kube-vip
|
||||
|
||||
manifest-test:
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --enableLoadBalancer
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --controlplane --services
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster --enableLoadBalancer
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --leaderElection --controlplane --services --inCluster
|
||||
|
||||
unit-tests:
|
||||
go test ./...
|
||||
|
||||
@@ -123,13 +133,21 @@ integration-tests:
|
||||
go test -tags=integration,e2e -v ./pkg/etcd
|
||||
|
||||
e2e-tests:
|
||||
E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run github.com/onsi/ginkgo/v2/ginkgo --tags=e2e -v -p ./testing/e2e ./testing/e2e/etcd
|
||||
GOMAXPROCS=4 E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run github.com/onsi/ginkgo/v2/ginkgo --tags=e2e -v -p ./testing/e2e ./testing/e2e/etcd
|
||||
|
||||
e2e-tests129:
|
||||
V129=true K8S_IMAGE_PATH=kindest/node:v1.29.0 E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run github.com/onsi/ginkgo/v2/ginkgo --tags=e2e -v -p ./testing/e2e
|
||||
e2e-tests129-arp:
|
||||
GOMAXPROCS=4 TEST_MODE=arp V129=true K8S_IMAGE_PATH=kindest/node:v1.29.0 E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run github.com/onsi/ginkgo/v2/ginkgo --tags=e2e -v -p ./testing/e2e
|
||||
|
||||
e2e-tests129-rt:
|
||||
GOMAXPROCS=4 TEST_MODE=rt V129=true K8S_IMAGE_PATH=kindest/node:v1.29.0 E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run github.com/onsi/ginkgo/v2/ginkgo --tags=e2e -v -p ./testing/e2e
|
||||
|
||||
e2e-tests129-bgp:
|
||||
GOMAXPROCS=4 TEST_MODE=bgp V129=true K8S_IMAGE_PATH=kindest/node:v1.29.0 E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run github.com/onsi/ginkgo/v2/ginkgo --tags=e2e -v -p ./testing/e2e
|
||||
|
||||
e2e-tests129: e2e-tests129-arp e2e-tests129-rt e2e-tests129-bgp
|
||||
|
||||
service-tests:
|
||||
E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run ./testing/e2e/services -Services
|
||||
E2E_IMAGE_PATH=$(REPOSITORY)/$(TARGET):$(DOCKERTAG) go run ./testing/services -Services -simple -deployments -leaderActive -leaderFailover -localDeploy -egress -egressIPv6 -dualStack
|
||||
|
||||
trivy: dockerx86ActionIPTables
|
||||
docker run -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.47.0 \
|
||||
@@ -141,3 +159,21 @@ trivy: dockerx86ActionIPTables
|
||||
--severity 'CRITICAL,HIGH' \
|
||||
$(REPOSITORY)/$(TARGET):action
|
||||
|
||||
kind-quick:
|
||||
echo "Standing up your cluster"
|
||||
kind create cluster --config ./testing/kind/kind.yaml --name kube-vip
|
||||
kubectl apply -f https://kube-vip.io/manifests/rbac.yaml
|
||||
kubectl create configmap --namespace kube-system kubevip --from-literal range-global=172.18.100.10-172.18.100.30
|
||||
kubectl apply -f https://raw.githubusercontent.com/kube-vip/kube-vip-cloud-provider/main/manifest/kube-vip-cloud-controller.yaml
|
||||
kind load docker-image --name kube-vip $(REPOSITORY)/$(TARGET):$(DOCKERTAG)
|
||||
docker run --network host --rm $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --services --inCluster --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --servicesElection --interface eth0 | kubectl apply -f -
|
||||
|
||||
kind-reload:
|
||||
kind load docker-image $(REPOSITORY)/$(TARGET):$(DOCKERTAG) --name services
|
||||
kubectl rollout restart -n kube-system daemonset/kube-vip-ds
|
||||
|
||||
get-gobgp:
|
||||
mkdir -p bin
|
||||
wget -nc --directory-prefix=bin https://github.com/osrg/gobgp/releases/download/v3.37.0/gobgp_3.37.0_linux_amd64.tar.gz
|
||||
tar -xvzf bin/gobgp_3.37.0_linux_amd64.tar.gz -C bin
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ High Availability and Load-Balancing
|
||||
|
||||

|
||||
|
||||
[](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml)
|
||||
[](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml) [](https://insights.linuxfoundation.org/project/kube-vip) [&message=212&color=0094FF&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/kube-vip)
|
||||
|
||||
## Overview
|
||||
Kubernetes Virtual IP and Load-Balancer for both control plane and Kubernetes services
|
||||
@@ -32,6 +32,7 @@ Kube-Vip was originally created to provide a HA solution for the Kubernetes cont
|
||||
- Service LoadBalancer address pools per namespace or global
|
||||
- Service LoadBalancer address via (existing network DHCP)
|
||||
- Service LoadBalancer address exposure to gateway via UPNP
|
||||
- Egress! Kube-vip will utilise a service loadbalancer as both the ingress and **egress** for a pod.
|
||||
- ... manifest generation, vendor API integrations and many more...
|
||||
|
||||
## Why?
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ func init() {
|
||||
var kubeKubeadm = &cobra.Command{
|
||||
Use: "kubeadm",
|
||||
Short: "Kubeadm functions",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
_ = cmd.Help()
|
||||
// TODO - A load of text detailing what's actually happening
|
||||
},
|
||||
@@ -32,28 +33,39 @@ var kubeKubeadmInit = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "kube-vip init",
|
||||
Long: "The \"init\" subcommand will generate the Kubernetes manifest that will be started by kubeadm through the kubeadm init process",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(logLevel))
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
|
||||
initConfig.LoadBalancers = append(initConfig.LoadBalancers, initLoadBalancer)
|
||||
// TODO - A load of text detailing what's actually happening
|
||||
err := kubevip.ParseEnvironment(&initConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing environment from config: %v", err)
|
||||
log.Error("parsing environment", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - check for certain things VIP/interfaces
|
||||
if initConfig.Interface == "" {
|
||||
_ = cmd.Help()
|
||||
log.Fatalln("No interface is specified for kube-vip to bind to")
|
||||
log.Error("No interface is specified for kube-vip to bind to")
|
||||
return
|
||||
}
|
||||
|
||||
if initConfig.VIP == "" && initConfig.Address == "" {
|
||||
_ = cmd.Help()
|
||||
log.Fatalln("No address is specified for kube-vip to expose services on")
|
||||
log.Error("No address is specified for kube-vip to expose services on")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating VIPSubnet", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, image, Release.Version, inCluster)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
@@ -61,33 +73,44 @@ var kubeKubeadmInit = &cobra.Command{
|
||||
var kubeKubeadmJoin = &cobra.Command{
|
||||
Use: "join",
|
||||
Short: "kube-vip join",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(logLevel))
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
|
||||
initConfig.LoadBalancers = append(initConfig.LoadBalancers, initLoadBalancer)
|
||||
// TODO - A load of text detailing what's actually happening
|
||||
err := kubevip.ParseEnvironment(&initConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing environment from config: %v", err)
|
||||
log.Error("parsing environment", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - check for certain things VIP/interfaces
|
||||
if initConfig.Interface == "" {
|
||||
_ = cmd.Help()
|
||||
log.Fatalln("No interface is specified for kube-vip to bind to")
|
||||
log.Error("No interface is specified for kube-vip to bind to")
|
||||
return
|
||||
}
|
||||
|
||||
if initConfig.VIP == "" && initConfig.Address == "" {
|
||||
_ = cmd.Help()
|
||||
log.Fatalln("No address is specified for kube-vip to expose services on")
|
||||
log.Error("No address is specified for kube-vip to expose services on")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(kubeConfigPath); os.IsNotExist(err) {
|
||||
log.Fatalf("Unable to find file [%s]", kubeConfigPath)
|
||||
log.Error("kubeConfig not found", "Path", kubeConfigPath)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating VIPSubnet", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, image, Release.Version, inCluster)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,23 +3,26 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// manifests will eventually deprecate the kubeadm set of subcommands
|
||||
// manifests will be used to generate:
|
||||
// - Pod spec manifest, mainly used for a static pod (kubeadm)
|
||||
// - Daemonset manifest, mainly used to run kube-vip as a deamonset within Kubernetes (k3s/rke)
|
||||
// - RBAC manifest, used to generate the RBAC permissions for kube-vip
|
||||
|
||||
// var inCluster bool
|
||||
var taint bool
|
||||
var taint, role, rolebinding bool
|
||||
|
||||
func init() {
|
||||
kubeManifest.PersistentFlags().BoolVar(&inCluster, "inCluster", false, "Use the incluster token to authenticate to Kubernetes")
|
||||
kubeManifest.PersistentFlags().StringVar(&image, "image", "ghcr.io/kube-vip/kube-vip", "Define a hardcoded image with or without tag for the manifest")
|
||||
kubeManifestDaemon.PersistentFlags().BoolVar(&taint, "taint", false, "Taint the manifest for only running on control planes")
|
||||
kubeManifestRbac.PersistentFlags().BoolVar(&role, "role", false, "Generate only a Role inside the serviceNamespace access")
|
||||
kubeManifestRbac.PersistentFlags().BoolVar(&rolebinding, "rolebinding", false, "Generate only a RoleBinding for namespaced access")
|
||||
|
||||
kubeManifest.AddCommand(kubeManifestPod)
|
||||
kubeManifest.AddCommand(kubeManifestDaemon)
|
||||
@@ -29,7 +32,7 @@ func init() {
|
||||
var kubeManifest = &cobra.Command{
|
||||
Use: "manifest",
|
||||
Short: "Manifest functions",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
_ = cmd.Help()
|
||||
// TODO - A load of text detailing what's actually happening
|
||||
},
|
||||
@@ -38,22 +41,33 @@ var kubeManifest = &cobra.Command{
|
||||
var kubeManifestPod = &cobra.Command{
|
||||
Use: "pod",
|
||||
Short: "Generate a Pod Manifest",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(logLevel))
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
var err error
|
||||
|
||||
initConfig.LoadBalancers = append(initConfig.LoadBalancers, initLoadBalancer)
|
||||
// TODO - A load of text detailing what's actually happening
|
||||
if err := kubevip.ParseEnvironment(&initConfig); err != nil {
|
||||
log.Fatalf("Error parsing environment from config: %v", err)
|
||||
log.Error("parsing environment", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// The control plane has a requirement for a VIP being specified
|
||||
if initConfig.EnableControlPlane && (initConfig.VIP == "" && initConfig.Address == "" && !initConfig.DDNS) {
|
||||
_ = cmd.Help()
|
||||
log.Fatalln("No address is specified for kube-vip to expose services on")
|
||||
log.Error("No address is specified for kube-vip to expose services on")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("config parse", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, image, Release.Version, inCluster)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
@@ -61,24 +75,32 @@ var kubeManifestPod = &cobra.Command{
|
||||
var kubeManifestDaemon = &cobra.Command{
|
||||
Use: "daemonset",
|
||||
Short: "Generate a Daemonset Manifest",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(logLevel))
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
var err error
|
||||
|
||||
initConfig.LoadBalancers = append(initConfig.LoadBalancers, initLoadBalancer)
|
||||
// TODO - A load of text detailing what's actually happening
|
||||
if err := kubevip.ParseEnvironment(&initConfig); err != nil {
|
||||
log.Fatalf("error parsing environment config: %v", err)
|
||||
log.Error("parsing environment", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - check for certain things VIP/interfaces
|
||||
|
||||
// The control plane has a requirement for a VIP being specified
|
||||
if initConfig.EnableControlPlane && (initConfig.VIP == "" && initConfig.Address == "" && !initConfig.DDNS) {
|
||||
_ = cmd.Help()
|
||||
log.Fatalln("No address is specified for kube-vip to expose services on")
|
||||
log.Error("No address is specified for kube-vip to expose services on")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := kubevip.GenerateDaemonsetManifestFromConfig(&initConfig, Release.Version, inCluster, taint)
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("config parse", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GenerateDaemonsetManifestFromConfig(&initConfig, image, Release.Version, inCluster, taint)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
@@ -86,22 +108,44 @@ var kubeManifestDaemon = &cobra.Command{
|
||||
var kubeManifestRbac = &cobra.Command{
|
||||
Use: "rbac",
|
||||
Short: "Generate an RBAC Manifest",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(logLevel))
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
var err error
|
||||
|
||||
initConfig.LoadBalancers = append(initConfig.LoadBalancers, initLoadBalancer)
|
||||
// TODO - A load of text detailing what's actually happening
|
||||
if err := kubevip.ParseEnvironment(&initConfig); err != nil {
|
||||
log.Fatalf("Error parsing environment from config: %v", err)
|
||||
log.Error("parsing environment", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// The control plane has a requirement for a VIP being specified
|
||||
if initConfig.EnableControlPlane && (initConfig.VIP == "" && initConfig.Address == "" && !initConfig.DDNS) {
|
||||
_ = cmd.Help()
|
||||
log.Fatalln("No address is specified for kube-vip to expose services on")
|
||||
log.Error("No address is specified for kube-vip to expose services on")
|
||||
return
|
||||
}
|
||||
cfg := kubevip.GenerateSA()
|
||||
b, _ := yaml.Marshal(cfg)
|
||||
fmt.Println(string(b)) // output manifest to stdout
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating VIPSubnet", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
saCfg := kubevip.GenerateSA(&initConfig)
|
||||
roleCfg := kubevip.GenerateRole(&initConfig, role)
|
||||
if role {
|
||||
rolebinding = true
|
||||
}
|
||||
roleBindingCfg := kubevip.GenerateRoleBinding(rolebinding, saCfg, roleCfg)
|
||||
|
||||
// Output the YAML manifests to stdout
|
||||
fmt.Println("---") // Separator for YAML documents
|
||||
fmt.Println(kubevip.TransformApplyObjectToManifest(saCfg))
|
||||
fmt.Println("---") // Separator for YAML documents
|
||||
fmt.Println(kubevip.TransformApplyObjectToManifest(roleCfg))
|
||||
fmt.Println("---") // Separator for YAML documents
|
||||
fmt.Println(kubevip.TransformApplyObjectToManifest(roleBindingCfg))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Start as a single node (no cluster), start as a leader in the cluster
|
||||
var startConfig kubevip.Config
|
||||
var startConfigLB kubevip.LoadBalancer
|
||||
var startLocalPeer, startKubeConfigPath string
|
||||
var inCluster bool
|
||||
|
||||
func init() {
|
||||
// Get the configuration file
|
||||
kubeVipStart.Flags().StringVarP(&configPath, "config", "c", "", "Path to a kube-vip configuration")
|
||||
kubeVipStart.Flags().BoolVarP(&disableVIP, "disableVIP", "d", false, "Disable the VIP functionality")
|
||||
|
||||
// Pointers so we can see if they're nil (and not called)
|
||||
kubeVipStart.Flags().StringVar(&startConfig.Interface, "interface", "eth0", "Name of the interface to bind to")
|
||||
kubeVipStart.Flags().StringVar(&startConfig.VIP, "vip", "192.168.0.1", "The Virtual IP address")
|
||||
kubeVipStart.Flags().StringVar(&startConfig.Address, "address", "", "an address (IP or DNS name) to use as a VIP")
|
||||
kubeVipStart.Flags().IntVar(&startConfig.Port, "port", 6443, "listen port for the VIP")
|
||||
kubeVipStart.Flags().BoolVar(&startConfig.DDNS, "ddns", false, "use Dynamic DNS + DHCP to allocate VIP for address")
|
||||
kubeVipStart.Flags().BoolVar(&startConfig.SingleNode, "singleNode", false, "Start this instance as a single node")
|
||||
kubeVipStart.Flags().BoolVar(&startConfig.StartAsLeader, "startAsLeader", false, "Start this instance as the cluster leader")
|
||||
kubeVipStart.Flags().BoolVar(&startConfig.EnableARP, "arp", false, "Use ARP broadcasts to improve VIP re-allocations")
|
||||
kubeVipStart.Flags().StringVar(&startLocalPeer, "localPeer", "server1:192.168.0.1:10000", "Settings for this peer, format: id:address:port")
|
||||
|
||||
// Load Balancer flags
|
||||
kubeVipStart.Flags().BoolVar(&startConfigLB.BindToVip, "lbBindToVip", false, "Bind example load balancer to VIP")
|
||||
kubeVipStart.Flags().StringVar(&startConfigLB.Type, "lbType", "tcp", "Type of load balancer instance (TCP/HTTP)")
|
||||
kubeVipStart.Flags().StringVar(&startConfigLB.Name, "lbName", "Example Load Balancer", "The name of a load balancer instance")
|
||||
kubeVipStart.Flags().IntVar(&startConfigLB.Port, "lbPort", 8080, "Port that load balancer will expose on")
|
||||
kubeVipStart.Flags().StringVar(&startConfigLB.ForwardingMethod, "lbForwardingMethod", "local", "The forwarding method of a load balancer instance")
|
||||
|
||||
// Cluster configuration
|
||||
kubeVipStart.Flags().StringVar(&startKubeConfigPath, "kubeConfig", "/etc/kubernetes/admin.conf", "The path of a kubernetes configuration file")
|
||||
kubeVipStart.Flags().BoolVar(&inCluster, "inCluster", false, "Use the incluster token to authenticate to Kubernetes")
|
||||
kubeVipStart.Flags().BoolVar(&startConfig.EnableLeaderElection, "leaderElection", false, "Use the Kubernetes leader election mechanism for clustering")
|
||||
|
||||
// This sets the namespace that the lock should exist in
|
||||
kubeVipStart.Flags().StringVarP(&startConfig.Namespace, "namespace", "n", "kube-system", "The configuration map defined within the cluster")
|
||||
}
|
||||
|
||||
var kubeVipStart = &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start the Virtual IP / Load balancer",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(logLevel))
|
||||
var err error
|
||||
|
||||
// If a configuration file is loaded, then it will overwrite flags
|
||||
|
||||
// parse environment variables, these will overwrite anything loaded or flags
|
||||
err = kubevip.ParseEnvironment(&startConfig)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if startConfig.LeaderElectionType == "etcd" {
|
||||
log.Fatalln("Leader election with etcd not supported in start command, use manager")
|
||||
}
|
||||
|
||||
newCluster, err := cluster.InitCluster(&startConfig, disableVIP)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
var bgpServer *bgp.Server
|
||||
if startConfig.SingleNode {
|
||||
// If the Virtual IP isn't disabled then create the netlink configuration
|
||||
// Start a single node cluster
|
||||
if err := newCluster.StartSingleNode(&startConfig, disableVIP); err != nil {
|
||||
log.Errorf("error starting single node: %v", err)
|
||||
}
|
||||
} else {
|
||||
if disableVIP {
|
||||
log.Fatalln("Cluster mode requires the Virtual IP to be enabled, use single node with no VIP")
|
||||
}
|
||||
|
||||
if startConfig.EnableLeaderElection {
|
||||
cm, err := cluster.NewManager(startKubeConfigPath, inCluster, startConfig.Port)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
if startConfig.EnableBGP {
|
||||
log.Info("Starting the BGP server to advertise VIP routes to VGP peers")
|
||||
bgpServer, err = bgp.NewBGPServer(&startConfig.BGPConfig, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
// Defer a function to check if the bgpServer has been created and if so attempt to close it
|
||||
defer func() {
|
||||
if bgpServer != nil {
|
||||
bgpServer.Close()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Leader Cluster will block
|
||||
err = newCluster.StartCluster(&startConfig, cm, bgpServer)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
284
cmd/kube-vip.go
284
cmd/kube-vip.go
@@ -3,46 +3,38 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/equinixmetal"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/manager"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
// Path to the configuration file
|
||||
var configPath string
|
||||
// Is an option to set the image
|
||||
var image string
|
||||
|
||||
// Path to the configuration file
|
||||
// var namespace string
|
||||
|
||||
// Disable the Virtual IP (bind to the existing network stack)
|
||||
var disableVIP bool
|
||||
|
||||
// Disable the Virtual IP (bind to the existing network stack)
|
||||
// var controlPlane bool
|
||||
|
||||
// Run as a load balancer service (within a pod / kubernetes)
|
||||
// var serviceArp bool
|
||||
// Is kube-vip running within cluster
|
||||
var inCluster bool
|
||||
|
||||
// ConfigMap name within a Kubernetes cluster
|
||||
var configMap string
|
||||
|
||||
// Configure the level of logging
|
||||
var logLevel uint32
|
||||
|
||||
// Provider Config
|
||||
var providerConfig string
|
||||
// Points to a kubernetes configuration file
|
||||
var kubeConfigPath string
|
||||
|
||||
// Release - this struct contains the release information populated when building kube-vip
|
||||
var Release struct {
|
||||
@@ -56,9 +48,6 @@ var (
|
||||
initLoadBalancer kubevip.LoadBalancer
|
||||
)
|
||||
|
||||
// Points to a kubernetes configuration file
|
||||
var kubeConfigPath string
|
||||
|
||||
var kubeVipCmd = &cobra.Command{
|
||||
Use: "kube-vip",
|
||||
Short: "This is a server for providing a Virtual IP and load-balancer for the Kubernetes control-plane",
|
||||
@@ -69,36 +58,31 @@ func init() {
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.Interface, "interface", "", "Name of the interface to bind to")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.ServicesInterface, "serviceInterface", "", "Name of the interface to bind to (for services)")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.VIP, "vip", "", "The Virtual IP address")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.VIPSubnet, "vipSubnet", "", "The Virtual IP address subnet e.g. /32 /24 /8 etc..")
|
||||
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.VIPCIDR, "cidr", "32", "The CIDR range for the virtual IP address") // todo: deprecate
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.VIPSubnet, "vipSubnet", "", "The Virtual IP address subnet e.g. /32 /24 /8 etc.. (Default to 32 for IPv4 and 128 for IPv6)")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.NodeName, "nodeName", "", "Name to be used for lease holder. Must be unique for each node/instance")
|
||||
|
||||
// VIP flags
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.Address, "address", "", "an address (IP or DNS name) to use as a VIP")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.Port, "port", 6443, "Port for the VIP")
|
||||
kubeVipCmd.PersistentFlags().Uint16Var(&initConfig.Port, "port", 6443, "Port for the VIP")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableARP, "arp", false, "Enable Arp for VIP changes")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableWireguard, "wireguard", false, "Enable Wireguard for services VIPs")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableRoutingTable, "table", false, "Enable Routing Table for services VIPs")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.PreserveVIPOnLeadershipLoss, "preserveVipOnLeadershipLoss", false, "Preserve ARP VIP addresses on interface when leadership is lost (default: false for backward compatibility)")
|
||||
|
||||
// LoadBalancer flags
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableLoadBalancer, "enableLoadBalancer", false, "enable loadbalancing on the VIP with IPVS")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.LoadBalancerPort, "lbPort", 6443, "loadbalancer port for the VIP")
|
||||
kubeVipCmd.PersistentFlags().Uint16Var(&initConfig.LoadBalancerPort, "lbPort", 6443, "loadbalancer port for the VIP")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.LoadBalancerForwardingMethod, "lbForwardingMethod", "local", "loadbalancer forwarding method")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.DDNS, "ddns", false, "use Dynamic DNS + DHCP to allocate VIP for address")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.MirrorDestInterface, "mirrorDestInterface", "", "network interface where all traffic that traverses the service interface will be mirrored to. Source interface will use default interface is servicesInterface is not set.")
|
||||
|
||||
// Clustering type (leaderElection)
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableLeaderElection, "leaderElection", false, "Use the Kubernetes leader election mechanism for clustering")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.LeaderElectionType, "leaderElectionType", "kubernetes", "Defines the backend to run the leader election: kubernetes or etcd. Defaults to kubernetes.")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.LeaseName, "leaseName", "plndr-cp-lock", "Name of the lease that is used for leader election")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.LeaseDuration, "leaseDuration", 5, "Length of time a Kubernetes leader lease can be held for")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RenewDeadline, "leaseRenewDuration", 3, "Length of time a Kubernetes leader can attempt to renew its lease")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RetryPeriod, "leaseRetry", 1, "Number of times the host will retry to hold a lease")
|
||||
|
||||
// Equinix Metal flags
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableMetal, "metal", false, "This will use the Equinix Metal API (requires the token ENV) to update the EIP <-> VIP")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.MetalAPIKey, "metalKey", "", "The API token for authenticating with the Equinix Metal API")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.MetalProject, "metalProject", "", "The name of project already created within Equinix Metal")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.MetalProjectID, "metalProjectID", "", "The ID of project already created within Equinix Metal")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.ProviderConfig, "provider-config", "", "The path to a provider configuration")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.LeaseDuration, "leaseDuration", 5, "Length of time (in seconds) a Kubernetes leader lease can be held for")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RenewDeadline, "leaseRenewDuration", 3, "Length of time (in seconds) a Kubernetes leader can attempt to renew its lease")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RetryPeriod, "leaseRetry", 1, "Length of time (in seconds) the LeaderElector clients should wait between tries of actions")
|
||||
|
||||
// BGP flags
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableBGP, "bgp", false, "This will enable BGP support within kube-vip")
|
||||
@@ -115,18 +99,23 @@ func init() {
|
||||
kubeVipCmd.PersistentFlags().StringSliceVar(&initConfig.BGPPeers, "bgppeers", []string{}, "Comma separated BGP Peer, format: address:as:password:multihop")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.Annotations, "annotations", "", "Set Node annotations prefix for parsing")
|
||||
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.BGPConfig.Zebra.Enabled, "zebra", false, "This will enable Zebra support within kube-vip")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.BGPConfig.Zebra.URL, "zebraUrl", "unix:/var/run/frr/zserv.api", "Path to the unix domain socket for connecting to Zebra daemon")
|
||||
kubeVipCmd.PersistentFlags().Uint32Var(&initConfig.BGPConfig.Zebra.Version, "zebraVersion", 6, "Zebra API Version")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.BGPConfig.Zebra.SoftwareName, "zebraSoftwareName", "frr8.3", "Software Name for Zebra")
|
||||
|
||||
// Namespace for kube-vip
|
||||
kubeVipCmd.PersistentFlags().StringVarP(&initConfig.Namespace, "namespace", "n", "kube-system", "The namespace for the configmap defined within the cluster")
|
||||
|
||||
// Manage logging
|
||||
kubeVipCmd.PersistentFlags().Uint32Var(&logLevel, "log", 4, "Set the level of logging")
|
||||
kubeVipCmd.PersistentFlags().Int32Var(&initConfig.Logging, "log", 0, "Set the level of logging")
|
||||
|
||||
// Service flags
|
||||
kubeVipService.Flags().StringVarP(&configMap, "configMap", "c", "plndr", "The configuration map defined within the cluster")
|
||||
|
||||
// Routing Table flags
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RoutingTableID, "tableID", 198, "The routing table used for all table entries")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RoutingTableType, "tableType", 0, "The type of route that will be added to the routing table")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RoutingTableType, "tableType", unix.RTN_UNICAST, "The type of route that will be added to the routing table")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.RoutingProtocol, "routingProtocol", 248, "The routing protocol value used to create routes")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.CleanRoutingTable, "cleanRoutingTable", false, "Clean routing table of redundant routes on start")
|
||||
|
||||
@@ -137,14 +126,17 @@ func init() {
|
||||
|
||||
// Extended behaviour flags
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableServicesElection, "servicesElection", false, "Enable leader election per kubernetes service")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoadBalancerClassOnly, "lbClassOnly", false, "Enable load balancing only for services with LoadBalancerClass \"kube-vip.io/kube-vip-class\"")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.LoadBalancerClassName, "lbClassName", "kube-vip.io/kube-vip-class", "Name of load balancer class for kube-VIP, defaults to \"kube-vip.io/kube-vip-class\"")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoadBalancerClassOnly, "lbClassOnly", false, fmt.Sprintf("Enable load balancing only for services with LoadBalancerClass %q", kubevip.LBClassName))
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.LoadBalancerClassName, "lbClassName", kubevip.LBClassName, fmt.Sprintf("Name of load balancer class for kube-VIP, defaults to %q", kubevip.LBClassName))
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoadBalancerClassLegacyHandling, "lbClassNameLegacyHandling", true, "Use legacy LoadBalancer class name handling (e.g. accepting services both with empty and non-empty class)")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableServiceSecurity, "onlyAllowTrafficServicePorts", false, "Only allow traffic to service ports, others will be dropped, defaults to false")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableNodeLabeling, "enableNodeLabeling", false, "Enable leader node labeling with \"kube-vip.io/has-ip=<VIP address>\", defaults to false")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableNodeLabeling, "enableNodeLabeling", false, fmt.Sprintf("Enable leader node labeling with %q, defaults to false", kubevip.HasIP))
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.ServicesLeaseName, "servicesLeaseName", "plndr-svcs-lock", "Name of the lease that is used for leader election for services (in arp mode)")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.DNSMode, "dnsMode", "first", "Name of the mode that DNS lookup will be performed (first, ipv4, ipv6, dual)")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.DisableServiceUpdates, "disableServiceUpdates", false, "If true, kube-vip will process services as usual, but will not update service's Status.LoadBalancer.Ingress slice")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableEndpointSlices, "enableEndpointSlices", false, "If enabled, kube-vip will only advertise services, but will use EndpointSlices instead of endpoints to get IPs of Pods")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableEndpoints, "enableEndpoints", false, "If enabled, kube-vip will only advertise services, but will use the (deprecated since v1.33) endpoints for IP addresses")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoInterfaceGlobalScope, "loInterfaceGlobalScope", false, "If true, kube-vip will set global scope when using the lo interface, otherwise a host scope will be used by default")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.HealthCheckPort, "healthCheckPort", 0, "If set to non-zero (> 1024), then this is the port that the healthcheck will listen on")
|
||||
|
||||
// Prometheus HTTP Server
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.PrometheusHTTPServer, "prometheusHTTPServer", ":2112", "Host and port used to expose Prometheus metrics via an HTTP server")
|
||||
@@ -156,16 +148,20 @@ func init() {
|
||||
kubeVipCmd.PersistentFlags().StringSliceVar(&initConfig.Etcd.Endpoints, "etcdEndpoints", nil, "Etcd member endpoints")
|
||||
|
||||
// Kubernetes client specific flags
|
||||
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.K8sConfigFile, "k8sConfigPath", "/etc/kubernetes/admin.conf", "Path to the configuration file used with the Kubernetes client")
|
||||
|
||||
// Configuration file flag
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.ConfigFile, "config-file", "", "Path to a JSON/YAML configuration file to load settings from")
|
||||
|
||||
kubeVipCmd.AddCommand(kubeKubeadm)
|
||||
kubeVipCmd.AddCommand(kubeManifest)
|
||||
kubeVipCmd.AddCommand(kubeVipManager)
|
||||
kubeVipCmd.AddCommand(kubeVipSample)
|
||||
kubeVipCmd.AddCommand(kubeVipService)
|
||||
kubeVipCmd.AddCommand(kubeVipStart)
|
||||
kubeVipCmd.AddCommand(kubeVipVersion)
|
||||
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLogLoggerLevel(log.Level(initConfig.Logging))
|
||||
}
|
||||
|
||||
// Execute - starts the command parsing process
|
||||
@@ -179,7 +175,7 @@ func Execute() {
|
||||
var kubeVipVersion = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Version and Release information about the Kubernetes Virtual IP Server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
fmt.Printf("Kube-VIP Release Information\n")
|
||||
fmt.Printf("Version: %s\n", Release.Version)
|
||||
fmt.Printf("Build: %s\n", Release.Build)
|
||||
@@ -189,7 +185,7 @@ var kubeVipVersion = &cobra.Command{
|
||||
var kubeVipSample = &cobra.Command{
|
||||
Use: "sample",
|
||||
Short: "Generate a Sample configuration",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
@@ -197,18 +193,35 @@ var kubeVipSample = &cobra.Command{
|
||||
var kubeVipService = &cobra.Command{
|
||||
Use: "service",
|
||||
Short: "Start the Virtual IP / Load balancer as a service within a Kubernetes cluster",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(logLevel))
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
|
||||
// parse environment variables, these will overwrite anything loaded or flags
|
||||
err := kubevip.ParseEnvironment(&initConfig)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
// Load configuration from file if specified (lowest priority)
|
||||
if initConfig.ConfigFile != "" {
|
||||
err := kubevip.MergeConfigFromFile(&initConfig, initConfig.ConfigFile)
|
||||
if err != nil {
|
||||
log.Error("loading config file", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parse environment variables, these will overwrite anything loaded from config file
|
||||
err := kubevip.ParseEnvironment(&initConfig)
|
||||
if err != nil {
|
||||
log.Error("parsing env", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Change RTN_UNSPEC to default type
|
||||
if initConfig.RoutingProtocol == unix.RTN_UNSPEC {
|
||||
initConfig.RoutingProtocol = unix.RTN_UNICAST
|
||||
}
|
||||
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLogLoggerLevel(log.Level(initConfig.Logging))
|
||||
|
||||
if err := initConfig.CheckInterface(); err != nil {
|
||||
log.Fatalln(err)
|
||||
log.Error("checking interface", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// User Environment variables as an option to make manifest clearer
|
||||
@@ -217,16 +230,27 @@ var kubeVipService = &cobra.Command{
|
||||
configMap = envConfigMap
|
||||
}
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating CIDR", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Define the new service manager
|
||||
mgr, err := manager.New(configMap, &initConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
log.Error("new manager", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the service manager, this will watch the config Map and construct kube-vip services for it
|
||||
err = mgr.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
log.Error("manager start", "err", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -234,19 +258,42 @@ var kubeVipService = &cobra.Command{
|
||||
var kubeVipManager = &cobra.Command{
|
||||
Use: "manager",
|
||||
Short: "Start the kube-vip manager",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// parse environment variables, these will overwrite anything loaded or flags
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
// Load configuration from file if specified (lowest priority)
|
||||
if initConfig.ConfigFile != "" {
|
||||
err := kubevip.MergeConfigFromFile(&initConfig, initConfig.ConfigFile)
|
||||
if err != nil {
|
||||
log.Error("loading config file", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parse environment variables, these will overwrite anything loaded from config file
|
||||
err := kubevip.ParseEnvironment(&initConfig)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
log.Error("parsing environment", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Change RTN_UNSPEC to default type
|
||||
if initConfig.RoutingProtocol == unix.RTN_UNSPEC {
|
||||
initConfig.RoutingProtocol = unix.RTN_UNICAST
|
||||
}
|
||||
|
||||
// Set the logging level for all subsequent functions
|
||||
log.SetLevel(log.Level(initConfig.Logging))
|
||||
log.SetLogLoggerLevel(log.Level(initConfig.Logging))
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("No interface is specified for kube-vip to bind to")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Welome messages
|
||||
log.Infof("Starting kube-vip.io [%s]", Release.Version)
|
||||
log.Debugf("Build kube-vip.io [%s]", Release.Build)
|
||||
log.Info("kube-vip.io", "version", Release.Version, "build", Release.Build)
|
||||
|
||||
// start prometheus server
|
||||
if initConfig.PrometheusHTTPServer != "" {
|
||||
@@ -274,11 +321,22 @@ var kubeVipManager = &cobra.Command{
|
||||
}
|
||||
|
||||
// Provide configuration to output/logging
|
||||
log.Infof("namespace [%s], Mode: [%s], Features(s): Control Plane:[%t], Services:[%t]", initConfig.Namespace, mode, initConfig.EnableControlPlane, initConfig.EnableServices)
|
||||
log.Info("starting", "namespace", initConfig.Namespace, "Mode", mode, "Control Plane", initConfig.EnableControlPlane, "Services", initConfig.EnableServices)
|
||||
|
||||
// End if nothing is enabled
|
||||
if !initConfig.EnableServices && !initConfig.EnableControlPlane {
|
||||
log.Fatalln("no features are enabled")
|
||||
log.Error("no features are enabled")
|
||||
return
|
||||
}
|
||||
|
||||
if !initConfig.EnableARP && strings.Contains(initConfig.VIPSubnet, kubevip.Auto) {
|
||||
log.Error("auto subnet discovery cannot be used outside ARP mode")
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(initConfig.VIPSubnet, kubevip.Auto) && initConfig.Address != "" {
|
||||
log.Error("auto subnet discovery cannot be used if VIP address was provided")
|
||||
return
|
||||
}
|
||||
|
||||
// If we're using wireguard then all traffic goes through the wg0 interface
|
||||
@@ -288,48 +346,55 @@ var kubeVipManager = &cobra.Command{
|
||||
initConfig.Interface = "wg0"
|
||||
}
|
||||
|
||||
log.Infof("configuring Wireguard networking")
|
||||
log.Info("configuring Wireguard networking")
|
||||
l, err := netlink.LinkByName(initConfig.Interface)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Link not found") {
|
||||
log.Warnf("interface \"%s\" doesn't exist, attempting to create wireguard interface", initConfig.Interface)
|
||||
log.Warn("attempting to create wireguard interface", "interface not found", initConfig.Interface)
|
||||
err = netlink.LinkAdd(&netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: initConfig.Interface}})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
log.Error("adding link", "err", err)
|
||||
return
|
||||
}
|
||||
l, err = netlink.LinkByName(initConfig.Interface)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
log.Error("finding link", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
err = netlink.LinkSetUp(l)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
log.Error("setting link UP", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
} else { // if we're not using Wireguard then we'll need to use an actual interface
|
||||
// Check if the interface needs auto-detecting
|
||||
if initConfig.Interface == "" {
|
||||
log.Infof("No interface is specified for VIP in config, auto-detecting default Interface")
|
||||
log.Info("No interface is specified for VIP in config, auto-detecting default Interface")
|
||||
defaultIF, err := vip.GetDefaultGatewayInterface()
|
||||
if err != nil {
|
||||
_ = cmd.Help()
|
||||
log.Fatalf("unable to detect default interface -> [%v]", err)
|
||||
log.Error("detecting interface", "err", err)
|
||||
return
|
||||
}
|
||||
initConfig.Interface = defaultIF.Name
|
||||
log.Infof("kube-vip will bind to interface [%s]", initConfig.Interface)
|
||||
log.Info("kube-vip bind", "interface", initConfig.Interface)
|
||||
|
||||
go func() {
|
||||
if err := vip.MonitorDefaultInterface(context.TODO(), defaultIF); err != nil {
|
||||
log.Fatalf("crash: %s", err.Error())
|
||||
|
||||
log.Error("interface monitor", "err", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
// Perform a check on th state of the interface
|
||||
// Perform a check on the state of the interface
|
||||
if err := initConfig.CheckInterface(); err != nil {
|
||||
log.Fatalln(err)
|
||||
log.Error("checking interface", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// User Environment variables as an option to make manifest clearer
|
||||
@@ -338,22 +403,11 @@ var kubeVipManager = &cobra.Command{
|
||||
configMap = envConfigMap
|
||||
}
|
||||
|
||||
// If Equinix Metal is enabled and there is a provider configuration passed
|
||||
if initConfig.EnableMetal {
|
||||
if providerConfig != "" {
|
||||
providerAPI, providerProject, err := equinixmetal.GetPacketConfig(providerConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
initConfig.MetalAPIKey = providerAPI
|
||||
initConfig.MetalProject = providerProject
|
||||
}
|
||||
}
|
||||
|
||||
// Define the new service manager
|
||||
mgr, err := manager.New(configMap, &initConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("configuring new Manager error -> %v", err)
|
||||
log.Error("new manager", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
prometheus.MustRegister(mgr.PrometheusCollector()...)
|
||||
@@ -361,7 +415,8 @@ var kubeVipManager = &cobra.Command{
|
||||
// Start the service manager, this will watch the config Map and construct kube-vip services for it
|
||||
err = mgr.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("starting new Manager error -> %v", err)
|
||||
log.Error("start manager", "err", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -376,7 +431,7 @@ func servePrometheusHTTPServer(ctx context.Context, config PrometheusHTTPServerC
|
||||
var err error
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { //nolint TODO
|
||||
_, _ = w.Write([]byte(`<html>
|
||||
<head><title>kube-vip</title></head>
|
||||
<body>
|
||||
@@ -394,15 +449,16 @@ func servePrometheusHTTPServer(ctx context.Context, config PrometheusHTTPServerC
|
||||
|
||||
go func() {
|
||||
if err = srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("listen:%+s\n", err)
|
||||
log.Error("prometheus HTTP server", "err", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("prometheus HTTP server started")
|
||||
log.Info("prometheus HTTP server started")
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
log.Printf("prometheus HTTP server stopped")
|
||||
log.Info("prometheus HTTP server stopped")
|
||||
|
||||
ctxShutDown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer func() {
|
||||
@@ -410,10 +466,46 @@ func servePrometheusHTTPServer(ctx context.Context, config PrometheusHTTPServerC
|
||||
}()
|
||||
|
||||
if err = srv.Shutdown(ctxShutDown); err != nil {
|
||||
log.Fatalf("server Shutdown Failed:%+s", err)
|
||||
log.Error("shutting down prometheus HTTP server", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err == http.ErrServerClosed {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateCidrRange(address string, dnsMode string) (string, error) {
|
||||
var cidrs []string
|
||||
|
||||
addresses := strings.Split(address, ",")
|
||||
for _, a := range addresses {
|
||||
ip := net.ParseIP(a)
|
||||
if ip == nil {
|
||||
// we probably are a DNS name
|
||||
ips, err := utils.LookupHost(a, dnsMode)
|
||||
if len(ips) == 0 || err != nil {
|
||||
return "", fmt.Errorf("invalid IP address: %s from [%s], %v", a, address, err)
|
||||
}
|
||||
for _, addr := range ips {
|
||||
ip = net.ParseIP(addr)
|
||||
if ip.To4() != nil {
|
||||
cidrs = append(cidrs, "32")
|
||||
} else {
|
||||
cidrs = append(cidrs, "128")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ip.To4() != nil {
|
||||
cidrs = append(cidrs, "32")
|
||||
} else {
|
||||
cidrs = append(cidrs, "128")
|
||||
}
|
||||
}
|
||||
}
|
||||
// compact as DNS could have a lot of addresses
|
||||
slices.Sort(cidrs)
|
||||
cidrs = slices.Compact(cidrs)
|
||||
slices.Reverse(cidrs)
|
||||
return strings.Join(cidrs, ","), nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if serverType == strings.ToLower("udp") {
|
||||
if serverType == "udp" {
|
||||
// Start the UDP echo server
|
||||
|
||||
ServerAddr, err := net.ResolveUDPAddr("udp", ":10002")
|
||||
@@ -60,7 +60,7 @@ func main() {
|
||||
fmt.Println("error: ", err)
|
||||
}
|
||||
|
||||
ServerConn.WriteTo(buf[0:n])
|
||||
ServerConn.WriteTo(buf[0:n], addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
example/deployment.yaml
Normal file
19
example/deployment.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- containerPort: 80
|
||||
15
example/service-interface-annotation.yaml
Normal file
15
example/service-interface-annotation.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx-interface-ens192-service
|
||||
annotations:
|
||||
kube-vip.io/serviceInterface: ens192
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
13
example/service.yaml
Normal file
13
example/service.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx-service
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
231
go.mod
231
go.mod
@@ -1,134 +1,165 @@
|
||||
module github.com/kube-vip/kube-vip
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.3
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/cloudflare/ipvs v0.10.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/cloudflare/ipvs v0.11.0
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/florianl/go-conntrack v0.4.0
|
||||
github.com/golang/protobuf v1.5.3
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/google/nftables v0.3.0
|
||||
github.com/gookit/slog v0.6.0
|
||||
github.com/huin/goupnp v1.3.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d
|
||||
github.com/jpillora/backoff v1.0.0
|
||||
github.com/kamhlos/upnp v0.0.0-20210324072331-5661950dff08
|
||||
github.com/mdlayher/ndp v1.0.1
|
||||
github.com/onsi/ginkgo/v2 v2.15.0
|
||||
github.com/onsi/gomega v1.30.0
|
||||
github.com/osrg/gobgp/v3 v3.22.0
|
||||
github.com/packethost/packngo v0.31.0
|
||||
github.com/mdlayher/ndp v1.1.0
|
||||
github.com/onsi/ginkgo/v2 v2.27.2
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/osrg/gobgp/v3 v3.37.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||
go.etcd.io/etcd/api/v3 v3.5.12
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.11
|
||||
go.etcd.io/etcd/client/v3 v3.5.11
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/exp v0.0.0-20231005195138-3e424a577f31
|
||||
golang.org/x/sys v0.17.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.29.1
|
||||
k8s.io/apimachinery v0.29.1
|
||||
k8s.io/client-go v0.29.1
|
||||
k8s.io/klog/v2 v2.120.1
|
||||
sigs.k8s.io/kind v0.22.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.etcd.io/etcd/api/v3 v3.6.6
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6
|
||||
go.etcd.io/etcd/client/v3 v3.6.6
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
k8s.io/api v0.34.2
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
sigs.k8s.io/kind v0.30.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
al.essio.dev/pkg/shellescape v1.5.1 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/eapache/channels v1.1.0 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.6.0 // indirect
|
||||
github.com/gookit/goutil v0.7.1 // indirect
|
||||
github.com/gookit/gsr v0.1.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/k-sone/critbitgo v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/packet v1.1.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.16.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tj/go-spin v1.1.0 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/xlab/c-for-go v0.0.0-20230906092656-a1822f0a09c1 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xlab/c-for-go v1.3.0 // indirect
|
||||
github.com/xlab/pkgconfig v0.0.0-20170226114623-cea12a0fd245 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/oauth2 v0.12.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/term v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
|
||||
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
|
||||
modernc.org/cc/v4 v4.1.0 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
modernc.org/cc/v4 v4.24.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/opt v0.1.4 // indirect
|
||||
modernc.org/sortutil v1.2.1 // indirect
|
||||
modernc.org/strutil v1.2.1 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
|
||||
207
pkg/arp/arp.go
Normal file
207
pkg/arp/arp.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package arp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
instances sync.Map
|
||||
config *kubevip.Config
|
||||
}
|
||||
|
||||
type Instance struct {
|
||||
network vip.Network
|
||||
ndp *vip.NdpResponder
|
||||
mu sync.Mutex
|
||||
counter int
|
||||
}
|
||||
|
||||
func NewManager(config *kubevip.Config) *Manager {
|
||||
return &Manager{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func NewInstance(network vip.Network, ndp *vip.NdpResponder) *Instance {
|
||||
return &Instance{
|
||||
ndp: ndp,
|
||||
network: network,
|
||||
counter: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Instance) Name() string {
|
||||
return i.network.ARPName()
|
||||
}
|
||||
|
||||
func (m *Manager) Insert(instance *Instance) {
|
||||
i, err := m.get(instance.Name())
|
||||
if err != nil {
|
||||
log.Error("[ARP manager] unable to insert instance", "err", err)
|
||||
return
|
||||
}
|
||||
if i == nil {
|
||||
log.Info("[ARP manager] inserting ARP/NDP instance", "name", instance.Name())
|
||||
m.instances.Store(instance.Name(), instance)
|
||||
} else {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
i.counter++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(instance *Instance) {
|
||||
m.RemoveWithIPDelete(instance, true)
|
||||
}
|
||||
|
||||
// RemoveOnLeadershipLoss removes an ARP instance when leadership is lost
|
||||
func (m *Manager) RemoveOnLeadershipLoss(instance *Instance) {
|
||||
// Use the inverse of PreserveVIPOnLeadershipLoss to decide whether to delete the IP
|
||||
// If preserve is true, don't delete IP (deleteIP = false)
|
||||
// If preserve is false, delete IP (deleteIP = true), This is the legacy behavior
|
||||
deleteIP := !m.config.PreserveVIPOnLeadershipLoss
|
||||
m.RemoveWithIPDelete(instance, deleteIP)
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveWithIPDelete(instance *Instance, deleteIP bool) {
|
||||
i, err := m.get(instance.Name())
|
||||
if err != nil {
|
||||
log.Error("[ARP manager] unable to remove the instance", "err", err)
|
||||
return
|
||||
}
|
||||
if i != nil {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
if i.counter > 1 {
|
||||
i.counter--
|
||||
} else {
|
||||
log.Info("[ARP manager] removing ARP/NDP instance", "name", instance.Name())
|
||||
if deleteIP {
|
||||
if _, err := instance.network.DeleteIP(); err != nil {
|
||||
log.Error("failed to delete IP", "address", instance.network.IP(), "err", err)
|
||||
}
|
||||
}
|
||||
m.instances.Delete(instance.Name())
|
||||
}
|
||||
} else {
|
||||
log.Warn("[ARP manager] unable to remove the instance - instance not found", "name", instance.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Count(name string) int {
|
||||
i, err := m.get(name)
|
||||
if err != nil {
|
||||
log.Error("[ARP manager] unable to count instance", "err", err)
|
||||
return -1
|
||||
}
|
||||
if i != nil {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
return i.counter
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *Manager) StartAdvertisement(ctx context.Context) {
|
||||
log.Info("[ARP manager] starting ARP/NDP advertisement")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done(): // if cancel() execute
|
||||
return
|
||||
default:
|
||||
m.instances.Range(func(_ any, instance any) bool {
|
||||
if i, ok := instance.(*Instance); ok {
|
||||
if i.counter > 0 {
|
||||
ensureIPAndSendGratuitous(i)
|
||||
} else {
|
||||
// this instance should not be advertised - delete the IP just in case...
|
||||
if _, err := i.network.DeleteIP(); err != nil {
|
||||
log.Error("[ARP manager] failed to delete IP", "address", i.network.IP(), "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if m.config.ArpBroadcastRate < 500 {
|
||||
log.Warn("[ARP manager] arp broadcast rate is too low", "rate (ms)", m.config.ArpBroadcastRate, "setting to (ms)", "3000")
|
||||
m.config.ArpBroadcastRate = 3000
|
||||
}
|
||||
time.Sleep(time.Duration(m.config.ArpBroadcastRate) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) get(name string) (*Instance, error) {
|
||||
i, exists := m.instances.Load(name)
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
inst, ok := i.(*Instance)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("value for name %q is not of Instance pointer type", name)
|
||||
}
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
// ensureIPAndSendGratuitous - adds IP to the interface if missing, and send
|
||||
// either a gratuitous ARP or gratuitous NDP. Re-adds the interface if it is IPv6
|
||||
// and in a dadfailed state.
|
||||
func ensureIPAndSendGratuitous(instance *Instance) {
|
||||
iface := instance.network.Interface()
|
||||
ipString := instance.network.IP()
|
||||
|
||||
// Check if IP is dadfailed
|
||||
if instance.network.IsDADFAIL() {
|
||||
log.Warn("IP address is in dadfailed state, removing config", "ip", ipString, "interface", iface)
|
||||
deleted, err := instance.network.DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted and recreating address with NODAD flag to skip DAD", "IP", ipString, "interface", iface)
|
||||
// Re-add immediately without DAD check since we're recovering from DADFAILED
|
||||
// The AddIP function will set IFA_F_NODAD flag for IPv6 addresses when skipDAD=true
|
||||
if _, err := instance.network.AddIP(false, true); err != nil {
|
||||
log.Error("failed to recreate address after DADFAILED", "IP", ipString, "interface", iface, "err", err)
|
||||
} else {
|
||||
log.Info("successfully recreated address after DADFAILED recovery", "IP", ipString, "interface", iface)
|
||||
}
|
||||
}
|
||||
// Return early after DADFAILED recovery to avoid double IP addition
|
||||
return
|
||||
}
|
||||
|
||||
// Normal case: add IP with precheck and normal DAD process
|
||||
if added, err := instance.network.AddIP(true, false); err != nil {
|
||||
log.Warn(err.Error())
|
||||
} else if added {
|
||||
log.Warn("Re-applied the VIP configuration", "ip", ipString, "interface", iface)
|
||||
}
|
||||
|
||||
if utils.IsIPv6(ipString) {
|
||||
// Gratuitous NDP, will broadcast new MAC <-> IPv6 address
|
||||
if instance.ndp == nil {
|
||||
log.Error("NDP responder was not created")
|
||||
} else {
|
||||
err := instance.ndp.SendGratuitous(ipString)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Gratuitous ARP, will broadcast to new MAC <-> IPv4 address
|
||||
err := vip.ARPSendGratuitous(ipString, iface)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
87
pkg/backend/backend.go
Normal file
87
pkg/backend/backend.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/k8s"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type Entry struct {
|
||||
Addr string
|
||||
Port uint16
|
||||
IsLocal bool
|
||||
}
|
||||
|
||||
type Map map[Entry]bool
|
||||
|
||||
func (e *Entry) Check() bool {
|
||||
var client *kubernetes.Clientset
|
||||
var err error
|
||||
var config *rest.Config
|
||||
|
||||
adminConfigPath := "/etc/kubernetes/admin.conf"
|
||||
// TODO: add one more switch case of homeConfigPath if there is such scenario in future
|
||||
// homeConfigPath := filepath.Join(os.Getenv("HOME"), ".kube", "config")
|
||||
|
||||
var k8sAddr string
|
||||
if utils.IsIPv4(e.Addr) {
|
||||
k8sAddr = fmt.Sprintf("%s:%v", e.Addr, e.Port)
|
||||
} else {
|
||||
k8sAddr = fmt.Sprintf("[%s]:%v", e.Addr, e.Port)
|
||||
}
|
||||
|
||||
switch {
|
||||
case utils.FileExists(adminConfigPath):
|
||||
config, err = k8s.NewRestConfig(adminConfigPath, false, k8sAddr)
|
||||
if err != nil {
|
||||
log.Error("create k8s REST config", "path", adminConfigPath, "err", err)
|
||||
return false
|
||||
}
|
||||
default:
|
||||
config, err = k8s.NewRestConfig("", true, k8sAddr)
|
||||
if err != nil {
|
||||
log.Error("create k8s REST config", "err", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
client, err = k8s.NewClientset(config)
|
||||
if err != nil {
|
||||
log.Error("create k8s client", "err", err)
|
||||
return false
|
||||
}
|
||||
|
||||
_, err = client.DiscoveryClient.ServerVersion()
|
||||
if err != nil {
|
||||
log.Error("discover k8s version", "err", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Watch(tickAction func(), interval int, stop chan struct{}) {
|
||||
if interval <= 0 {
|
||||
interval = 5
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
ticker.Stop()
|
||||
tickAction()
|
||||
ticker.Reset(time.Second * time.Duration(interval))
|
||||
}
|
||||
}
|
||||
}
|
||||
223
pkg/bgp/peers.go
223
pkg/bgp/peers.go
@@ -4,16 +4,19 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/ptypes" //nolint
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
//nolint
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
api "github.com/osrg/gobgp/v3/api"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/osrg/gobgp/v3/pkg/server"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// AddPeer will add peers to the BGP configuration
|
||||
func (b *Server) AddPeer(peer Peer) (err error) {
|
||||
func (b *Server) AddPeer(peer kubevip.BGPPeer) (err error) {
|
||||
p := &api.Peer{
|
||||
Conf: &api.PeerConf{
|
||||
NeighborAddress: peer.Address,
|
||||
@@ -42,36 +45,94 @@ func (b *Server) AddPeer(peer Peer) (err error) {
|
||||
},
|
||||
}
|
||||
|
||||
if b.c.SourceIP != "" {
|
||||
p.Transport.LocalAddress = b.c.SourceIP
|
||||
if b.c.MpbgpNexthop != "" {
|
||||
p.AfiSafis = []*api.AfiSafi{
|
||||
{
|
||||
Config: &api.AfiSafiConfig{
|
||||
Family: &api.Family{
|
||||
Afi: api.Family_AFI_IP,
|
||||
Safi: api.Family_SAFI_UNICAST,
|
||||
},
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Config: &api.AfiSafiConfig{
|
||||
Family: &api.Family{
|
||||
Afi: api.Family_AFI_IP6,
|
||||
Safi: api.Family_SAFI_UNICAST,
|
||||
},
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
peer.SetMpbgpOptions(b.c)
|
||||
|
||||
ipv4Address, ipv6Address, err := peer.FindMpbgpAddresses(p, b.c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get MP-BGP addresses: %w", err)
|
||||
}
|
||||
|
||||
mask := "128"
|
||||
address := ipv4Address
|
||||
family := api.Family_AFI_IP
|
||||
if utils.IsIPv4(p.Conf.NeighborAddress) {
|
||||
mask = "32"
|
||||
address = ipv6Address
|
||||
family = api.Family_AFI_IP6
|
||||
}
|
||||
|
||||
err = b.s.AddDefinedSet(context.Background(), &api.AddDefinedSetRequest{
|
||||
DefinedSet: &api.DefinedSet{
|
||||
DefinedType: api.DefinedType_NEIGHBOR,
|
||||
Name: fmt.Sprintf("peer-%s", p.Conf.NeighborAddress),
|
||||
List: []string{fmt.Sprintf("%s/%s", p.Conf.NeighborAddress, mask)},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add defined set: %v", err)
|
||||
}
|
||||
|
||||
if address != "" {
|
||||
if err := insertPolicy(b.s, address, p, family); err != nil {
|
||||
return fmt.Errorf("failed to add policy: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if b.c.SourceIP != "" {
|
||||
p.Transport.LocalAddress = b.c.SourceIP
|
||||
}
|
||||
|
||||
if b.c.SourceIF != "" {
|
||||
p.Transport.BindInterface = b.c.SourceIF
|
||||
}
|
||||
}
|
||||
|
||||
if b.c.SourceIF != "" {
|
||||
p.Transport.BindInterface = b.c.SourceIF
|
||||
if err := b.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: p}); err != nil {
|
||||
return fmt.Errorf("failed to add peer: %v", err)
|
||||
}
|
||||
|
||||
return b.s.AddPeer(context.Background(), &api.AddPeerRequest{
|
||||
Peer: p,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Server) getPath(ip net.IP) (path *api.Path) {
|
||||
isV6 := ip.To4() == nil
|
||||
|
||||
//nolint
|
||||
originAttr, _ := ptypes.MarshalAny(&api.OriginAttribute{
|
||||
originAttr, _ := anypb.New(&api.OriginAttribute{
|
||||
Origin: 0,
|
||||
})
|
||||
|
||||
if !isV6 {
|
||||
//nolint
|
||||
nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{
|
||||
nlri, _ := anypb.New(&api.IPAddressPrefix{
|
||||
Prefix: ip.String(),
|
||||
PrefixLen: 32,
|
||||
})
|
||||
|
||||
//nolint
|
||||
nhAttr, _ := ptypes.MarshalAny(&api.NextHopAttribute{
|
||||
nhAttr, _ := anypb.New(&api.NextHopAttribute{
|
||||
NextHop: "0.0.0.0", // gobgp will fill this
|
||||
})
|
||||
|
||||
@@ -81,11 +142,11 @@ func (b *Server) getPath(ip net.IP) (path *api.Path) {
|
||||
Safi: api.Family_SAFI_UNICAST,
|
||||
},
|
||||
Nlri: nlri,
|
||||
Pattrs: []*any.Any{originAttr, nhAttr},
|
||||
Pattrs: []*anypb.Any{originAttr, nhAttr},
|
||||
}
|
||||
} else {
|
||||
//nolint
|
||||
nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{
|
||||
nlri, _ := anypb.New(&api.IPAddressPrefix{
|
||||
Prefix: ip.String(),
|
||||
PrefixLen: 128,
|
||||
})
|
||||
@@ -96,80 +157,88 @@ func (b *Server) getPath(ip net.IP) (path *api.Path) {
|
||||
}
|
||||
|
||||
//nolint
|
||||
mpAttr, _ := ptypes.MarshalAny(&api.MpReachNLRIAttribute{
|
||||
mpAttr, _ := anypb.New(&api.MpReachNLRIAttribute{
|
||||
Family: v6Family,
|
||||
NextHops: []string{"::"}, // gobgp will fill this
|
||||
Nlris: []*any.Any{nlri},
|
||||
Nlris: []*anypb.Any{nlri},
|
||||
})
|
||||
|
||||
path = &api.Path{
|
||||
Family: v6Family,
|
||||
Nlri: nlri,
|
||||
Pattrs: []*any.Any{originAttr, mpAttr},
|
||||
Pattrs: []*anypb.Any{originAttr, mpAttr},
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseBGPPeerConfig - take a string and parses it into an array of peers
|
||||
func ParseBGPPeerConfig(config string) (bgpPeers []Peer, err error) {
|
||||
peers := strings.Split(config, ",")
|
||||
if len(peers) == 0 {
|
||||
return nil, fmt.Errorf("No BGP Peer configurations found")
|
||||
func insertPolicy(s *server.BgpServer, address string, p *api.Peer, family api.Family_Afi) error {
|
||||
familyType := "v4"
|
||||
if family == api.Family_AFI_IP6 {
|
||||
familyType = "v6"
|
||||
}
|
||||
|
||||
for x := range peers {
|
||||
peerStr := peers[x]
|
||||
if peerStr == "" {
|
||||
continue
|
||||
}
|
||||
isV6Peer := peerStr[0] == '['
|
||||
setName := fmt.Sprintf("peer-%s", p.Conf.NeighborAddress)
|
||||
policyName := fmt.Sprintf("%s-%s", setName, familyType)
|
||||
|
||||
address := ""
|
||||
if isV6Peer {
|
||||
addressEndPos := strings.IndexByte(peerStr, ']')
|
||||
if addressEndPos == -1 {
|
||||
return nil, fmt.Errorf("no matching ] found for IPv6 BGP Peer")
|
||||
}
|
||||
address = peerStr[1:addressEndPos]
|
||||
peerStr = peerStr[addressEndPos+1:]
|
||||
}
|
||||
|
||||
peer := strings.Split(peerStr, ":")
|
||||
if len(peer) < 2 {
|
||||
return nil, fmt.Errorf("mandatory peering params <host>:<AS> incomplete")
|
||||
}
|
||||
|
||||
if !isV6Peer {
|
||||
address = peer[0]
|
||||
}
|
||||
|
||||
ASNumber, err := strconv.ParseUint(peer[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP Peer AS format error [%s]", peer[1])
|
||||
}
|
||||
|
||||
password := ""
|
||||
if len(peer) >= 3 {
|
||||
password = peer[2]
|
||||
}
|
||||
|
||||
multiHop := false
|
||||
if len(peer) >= 4 {
|
||||
multiHop, err = strconv.ParseBool(peer[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP MultiHop format error (true/false) [%s]", peer[1])
|
||||
}
|
||||
}
|
||||
|
||||
peerConfig := Peer{
|
||||
Address: address,
|
||||
AS: uint32(ASNumber),
|
||||
Password: password,
|
||||
MultiHop: multiHop,
|
||||
}
|
||||
|
||||
bgpPeers = append(bgpPeers, peerConfig)
|
||||
policy := &api.Policy{
|
||||
Name: policyName,
|
||||
Statements: []*api.Statement{
|
||||
{
|
||||
Conditions: &api.Conditions{
|
||||
AfiSafiIn: []*api.Family{
|
||||
{
|
||||
Afi: family,
|
||||
Safi: api.Family_SAFI_UNICAST,
|
||||
},
|
||||
},
|
||||
NeighborSet: &api.MatchSet{
|
||||
Type: api.MatchSet_ANY,
|
||||
Name: setName,
|
||||
},
|
||||
},
|
||||
Actions: &api.Actions{
|
||||
RouteAction: api.RouteAction_ACCEPT,
|
||||
Nexthop: &api.NexthopAction{
|
||||
Address: address,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Conditions: &api.Conditions{
|
||||
NeighborSet: &api.MatchSet{
|
||||
Type: api.MatchSet_ANY,
|
||||
Name: setName,
|
||||
},
|
||||
},
|
||||
Actions: &api.Actions{
|
||||
RouteAction: api.RouteAction_ACCEPT,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return
|
||||
|
||||
err := s.AddPolicy(context.Background(), &api.AddPolicyRequest{
|
||||
Policy: policy,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add policy: %w", err)
|
||||
}
|
||||
|
||||
err = s.AddPolicyAssignment(context.Background(), &api.AddPolicyAssignmentRequest{
|
||||
Assignment: &api.PolicyAssignment{
|
||||
Name: "global",
|
||||
Direction: api.PolicyDirection_EXPORT,
|
||||
Policies: []*api.Policy{
|
||||
{
|
||||
Name: policy.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add policy assignment: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
56
pkg/bgp/peers_test.go
Normal file
56
pkg/bgp/peers_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package bgp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
)
|
||||
|
||||
func TestParseBGPPeerConfig(t *testing.T) {
|
||||
type args struct {
|
||||
config string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantBgpPeers []kubevip.BGPPeer
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4, default port",
|
||||
args: args{config: "192.168.0.10:65000::false,192.168.0.11:65000::false"},
|
||||
wantBgpPeers: []kubevip.BGPPeer{
|
||||
{Address: "192.168.0.10", Port: 179, AS: 65000, MultiHop: false},
|
||||
{Address: "192.168.0.11", Port: 179, AS: 65000, MultiHop: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv4, different port",
|
||||
args: args{config: "192.168.0.10:65000::false:180,192.168.0.11:65000::false:190"},
|
||||
wantBgpPeers: []kubevip.BGPPeer{
|
||||
{Address: "192.168.0.10", Port: 180, AS: 65000, MultiHop: false},
|
||||
{Address: "192.168.0.11", Port: 190, AS: 65000, MultiHop: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6, multi-protocol",
|
||||
args: args{config: "[fd00:1111:2222:3333:c7d9:7235:6bf7:5d52]:65501::false/mpbgp_nexthop=auto_sourceif"},
|
||||
wantBgpPeers: []kubevip.BGPPeer{
|
||||
{Address: "fd00:1111:2222:3333:c7d9:7235:6bf7:5d52", Port: 179, AS: 65501, MultiHop: false, MpbgpNexthop: "auto_sourceif"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotBgpPeers, err := kubevip.ParseBGPPeerConfig(tt.args.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseBGPPeerConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(gotBgpPeers, tt.wantBgpPeers) {
|
||||
t.Errorf("ParseBGPPeerConfig() = %v, want %v", gotBgpPeers, tt.wantBgpPeers)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,35 +5,60 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
api "github.com/osrg/gobgp/v3/api"
|
||||
gobgp "github.com/osrg/gobgp/v3/pkg/server"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Server manages a server object
|
||||
type Server struct {
|
||||
s *gobgp.BgpServer
|
||||
c *kubevip.BGPConfig
|
||||
|
||||
// This is a prometheus gauge indicating the state of the sessions.
|
||||
// 1 means "ESTABLISHED", 0 means "NOT ESTABLISHED"
|
||||
BGPSessionInfoGauge *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// NewBGPServer takes a configuration and returns a running BGP server instance
|
||||
func NewBGPServer(c *Config, peerStateChangeCallback func(*api.WatchEventResponse_PeerEvent)) (b *Server, err error) {
|
||||
func NewBGPServer(c kubevip.BGPConfig) (b *Server, err error) {
|
||||
if c.AS == 0 {
|
||||
return nil, fmt.Errorf("You need to provide AS")
|
||||
return nil, fmt.Errorf("you need to provide AS")
|
||||
}
|
||||
|
||||
if c.SourceIP != "" && c.SourceIF != "" {
|
||||
return nil, fmt.Errorf("SourceIP and SourceIF are mutually exclusive")
|
||||
return nil, fmt.Errorf("sourceIP and SourceIF are mutually exclusive")
|
||||
}
|
||||
|
||||
if len(c.Peers) == 0 {
|
||||
return nil, fmt.Errorf("You need to provide at least one peer")
|
||||
return nil, fmt.Errorf("you need to provide at least one peer")
|
||||
}
|
||||
|
||||
b = &Server{
|
||||
s: gobgp.NewBgpServer(),
|
||||
c: c,
|
||||
c: &c,
|
||||
|
||||
BGPSessionInfoGauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "kube_vip",
|
||||
Subsystem: "manager",
|
||||
Name: "bgp_session_info",
|
||||
Help: "Display state of session by setting metric for label value with current state to 1",
|
||||
}, []string{"state", "peer"}),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Start starts the BGP server
|
||||
func (b *Server) Start(peerStateChangeCallback func(*api.WatchEventResponse_PeerEvent)) (err error) {
|
||||
go b.s.Serve()
|
||||
|
||||
if err = b.s.StartBgp(context.Background(), &api.StartBgpRequest{
|
||||
Global: &api.Global{
|
||||
Asn: c.AS,
|
||||
RouterId: c.RouterID,
|
||||
Asn: b.c.AS,
|
||||
RouterId: b.c.RouterID,
|
||||
ListenPort: -1,
|
||||
},
|
||||
}); err != nil {
|
||||
@@ -42,7 +67,7 @@ func NewBGPServer(c *Config, peerStateChangeCallback func(*api.WatchEventRespons
|
||||
|
||||
if err = b.s.WatchEvent(context.Background(), &api.WatchEventRequest{Peer: &api.WatchEventRequest_Peer{}}, func(r *api.WatchEventResponse) {
|
||||
if p := r.GetPeer(); p != nil && p.Type == api.WatchEventResponse_PeerEvent_STATE {
|
||||
log.Infof("[BGP] %s", p.String())
|
||||
log.Info("[BGP]", "peer", p.String())
|
||||
if peerStateChangeCallback != nil {
|
||||
peerStateChangeCallback(p)
|
||||
}
|
||||
@@ -51,12 +76,23 @@ func NewBGPServer(c *Config, peerStateChangeCallback func(*api.WatchEventRespons
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range c.Peers {
|
||||
for _, p := range b.c.Peers {
|
||||
if err = b.AddPeer(p); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.c.Zebra.Enabled {
|
||||
if err = b.s.EnableZebra(context.Background(), &api.EnableZebraRequest{
|
||||
Url: b.c.Zebra.URL,
|
||||
Version: b.c.Zebra.Version,
|
||||
SoftwareName: b.c.Zebra.SoftwareName,
|
||||
}); err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package bgp
|
||||
|
||||
import gobgp "github.com/osrg/gobgp/v3/pkg/server"
|
||||
|
||||
// Peer defines a BGP Peer
|
||||
type Peer struct {
|
||||
Address string
|
||||
AS uint32
|
||||
Password string
|
||||
MultiHop bool
|
||||
}
|
||||
|
||||
// Config defines the BGP server configuration
|
||||
type Config struct {
|
||||
AS uint32
|
||||
RouterID string
|
||||
SourceIP string
|
||||
SourceIF string
|
||||
|
||||
HoldTime uint64
|
||||
KeepaliveInterval uint64
|
||||
|
||||
Peers []Peer
|
||||
}
|
||||
|
||||
// Server manages a server object
|
||||
type Server struct {
|
||||
s *gobgp.BgpServer
|
||||
c *Config
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package cluster
|
||||
import (
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
@@ -15,16 +17,17 @@ type Cluster struct {
|
||||
completed chan bool
|
||||
once sync.Once
|
||||
Network []vip.Network
|
||||
arpMgr *arp.Manager
|
||||
}
|
||||
|
||||
// InitCluster - Will attempt to initialise all of the required settings for the cluster
|
||||
func InitCluster(c *kubevip.Config, disableVIP bool) (*Cluster, error) {
|
||||
func InitCluster(c *kubevip.Config, disableVIP bool, intfMgr *networkinterface.Manager, arpMgr *arp.Manager) (*Cluster, error) {
|
||||
var networks []vip.Network
|
||||
var err error
|
||||
|
||||
if !disableVIP {
|
||||
// Start the Virtual IP Networking configuration
|
||||
networks, err = startNetworking(c)
|
||||
networks, err = startNetworking(c, intfMgr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -32,25 +35,28 @@ func InitCluster(c *kubevip.Config, disableVIP bool) (*Cluster, error) {
|
||||
// Initialise the Cluster structure
|
||||
newCluster := &Cluster{
|
||||
Network: networks,
|
||||
arpMgr: arpMgr,
|
||||
}
|
||||
|
||||
log.Debugf("init enable service security: %t", c.EnableServiceSecurity)
|
||||
log.Debug("service security", "enabled", c.EnableServiceSecurity)
|
||||
|
||||
return newCluster, nil
|
||||
}
|
||||
|
||||
func startNetworking(c *kubevip.Config) ([]vip.Network, error) {
|
||||
func startNetworking(c *kubevip.Config, intfMgr *networkinterface.Manager) ([]vip.Network, error) {
|
||||
address := c.VIP
|
||||
|
||||
if c.Address != "" {
|
||||
address = c.Address
|
||||
}
|
||||
|
||||
addresses := vip.GetIPs(address)
|
||||
addresses := vip.Split(address)
|
||||
|
||||
networks := []vip.Network{}
|
||||
for _, addr := range addresses {
|
||||
network, err := vip.NewConfig(addr, c.Interface, c.VIPSubnet, c.DDNS, c.RoutingTableID, c.RoutingTableType, c.RoutingProtocol, c.DNSMode)
|
||||
network, err := vip.NewConfig(addr, c.Interface, c.LoInterfaceGlobalScope, c.VIPSubnet, c.DDNS, c.RoutingTableID,
|
||||
c.RoutingTableType, c.RoutingProtocol, c.DNSMode, c.LoadBalancerForwardingMethod, c.IptablesBackend,
|
||||
c.EnableLoadBalancer, c.EnableServiceSecurity, intfMgr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -12,16 +12,14 @@ import (
|
||||
// during runtime if IP changes, startDDNS don't have to do reconfigure because
|
||||
// dnsUpdater already have the functionality to keep trying resolve the IP
|
||||
// and update the VIP configuration if it changes
|
||||
func (cluster *Cluster) StartDDNS(ctx context.Context) error {
|
||||
for i := range cluster.Network {
|
||||
ddnsMgr := vip.NewDDNSManager(ctx, cluster.Network[i])
|
||||
ip, err := ddnsMgr.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cluster.Network[i].SetIP(ip); err != nil {
|
||||
return err
|
||||
}
|
||||
func (cluster *Cluster) StartDDNS(ctx context.Context, network vip.Network) error {
|
||||
ddnsMgr := vip.NewDDNSManager(ctx, network)
|
||||
ip, err := ddnsMgr.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = network.SetIP(ip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -11,15 +11,14 @@ import (
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/equinixmetal"
|
||||
"github.com/kube-vip/kube-vip/pkg/etcd"
|
||||
"github.com/kube-vip/kube-vip/pkg/k8s"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/loadbalancer"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
|
||||
"github.com/packethost/packngo"
|
||||
log "log/slog"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -34,7 +33,8 @@ import (
|
||||
|
||||
// Manager degines the manager of the load-balancing services
|
||||
type Manager struct {
|
||||
KubernetesClient *kubernetes.Clientset
|
||||
KubernetesClient *kubernetes.Clientset
|
||||
RetryWatcherClient *kubernetes.Clientset
|
||||
// This channel is used to signal a shutdown
|
||||
SignalChan chan os.Signal
|
||||
|
||||
@@ -64,24 +64,38 @@ func NewManager(path string, inCluster bool, port int) (*Manager, error) {
|
||||
hostname = fmt.Sprintf("%s:%v", id, port)
|
||||
}
|
||||
|
||||
clientset, err := k8s.NewClientset(path, inCluster, hostname)
|
||||
config, err := k8s.NewRestConfig(path, inCluster, hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create k8s REST config: %w", err)
|
||||
}
|
||||
|
||||
clientset, err := k8s.NewClientset(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating a new k8s clientset: %v", err)
|
||||
}
|
||||
|
||||
rwConfig, err := k8s.NewRestConfig(path, inCluster, hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create k8s REST config for retryClientSet: %w", err)
|
||||
}
|
||||
|
||||
rwConfig.Timeout = 0 // empty value to disable the timeout
|
||||
rwClientSet, err := k8s.NewClientset(rwConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create k8s client for retry watcher: %w", err)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
KubernetesClient: clientset,
|
||||
KubernetesClient: clientset,
|
||||
RetryWatcherClient: rwClientSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StartCluster - Begins a running instance of the Leader Election cluster
|
||||
func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *bgp.Server) error {
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
|
||||
log.Infof("Beginning cluster membership, namespace [%s], lock name [%s], id [%s]", c.Namespace, c.LeaseName, id)
|
||||
log.Info("cluster membership", "namespace", c.Namespace, "lock", c.LeaseName, "id", c.NodeName)
|
||||
|
||||
// use a Go context so we can tell the leaderelection code when we
|
||||
// want to step down
|
||||
@@ -119,9 +133,12 @@ func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *
|
||||
// (attempt to) Remove the virtual IP, in case it already exists
|
||||
|
||||
for i := range cluster.Network {
|
||||
err = cluster.Network[i].DeleteIP()
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Errorf("could not delete virtualIP: %v", err)
|
||||
log.Error("could not delete virtualIP", "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,53 +149,47 @@ func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *
|
||||
}
|
||||
}()
|
||||
|
||||
// If Equinix Metal is enabled then we can begin our preparation work
|
||||
var packetClient *packngo.Client
|
||||
if c.EnableMetal {
|
||||
if c.ProviderConfig != "" {
|
||||
key, project, err := equinixmetal.GetPacketConfig(c.ProviderConfig)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
// Set the environment variable with the key for the project
|
||||
os.Setenv("PACKET_AUTH_TOKEN", key)
|
||||
// Update the configuration with the project key
|
||||
c.MetalProjectID = project
|
||||
}
|
||||
}
|
||||
packetClient, err = packngo.NewClient()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
// We're using Equinix Metal with BGP, populate the Peer information from the API
|
||||
if c.EnableBGP {
|
||||
log.Infoln("Looking up the BGP configuration from Equinix Metal")
|
||||
err = equinixmetal.BGPLookup(packetClient, c)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.EnableBGP && bgpServer == nil {
|
||||
// Lets start BGP
|
||||
log.Info("Starting the BGP server to advertise VIP routes to VGP peers")
|
||||
bgpServer, err = bgp.NewBGPServer(&c.BGPConfig, nil)
|
||||
bgpServer, err = bgp.NewBGPServer(c.BGPConfig)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Error("new BGP server", "err", err)
|
||||
}
|
||||
if err := bgpServer.Start(nil); err != nil {
|
||||
log.Error("starting BGP server", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
run := &runConfig{
|
||||
config: c,
|
||||
leaseID: id,
|
||||
leaseID: c.NodeName,
|
||||
sm: sm,
|
||||
onStartedLeading: func(ctx context.Context) {
|
||||
onStartedLeading: func(ctx context.Context) { //nolint TODO: potential clean code
|
||||
// When we become leader, ensure we can take over VIPs even if they're preserved on other nodes
|
||||
if c.PreserveVIPOnLeadershipLoss {
|
||||
log.Info("Becoming leader with VIP preservation enabled - ensuring VIP takeover")
|
||||
// Force add the VIPs (this will work even if they exist due to the precheck logic)
|
||||
for i := range cluster.Network {
|
||||
added, err := cluster.Network[i].AddIP(true, false)
|
||||
if err != nil {
|
||||
log.Error("failed to ensure VIP on leader takeover", "vip", cluster.Network[i].IP(), "err", err)
|
||||
} else if added {
|
||||
log.Info("took over VIP as new leader", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
} else {
|
||||
log.Info("VIP already configured on interface", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start ARP advertisements now that we have leadership
|
||||
log.Info("Start ARP/NDP advertisement")
|
||||
go cluster.arpMgr.StartAdvertisement(ctxArp)
|
||||
|
||||
// As we're leading lets start the vip service
|
||||
err := cluster.vipService(ctxArp, ctxDNS, c, sm, bgpServer, packetClient)
|
||||
err := cluster.vipService(ctxArp, ctxDNS, c, sm, bgpServer, cancel)
|
||||
if err != nil {
|
||||
log.Errorf("Error starting the VIP service on the leader [%s]", err)
|
||||
log.Error("starting VIP service on leader", "err", err)
|
||||
}
|
||||
},
|
||||
onStoppedLeading: func() {
|
||||
@@ -194,22 +205,66 @@ func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *
|
||||
if bgpServer != nil {
|
||||
err := bgpServer.Close()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
log.Warn("close BGP server", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range cluster.Network {
|
||||
err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
// Handle VIP cleanup based on configuration
|
||||
if c.PreserveVIPOnLeadershipLoss {
|
||||
// For IPv6, we must remove VIPs immediately to avoid DAD failures on the new leader
|
||||
// IPv6 Duplicate Address Detection will fail if the new leader tries to add an IP that is still present on this node's interface
|
||||
// We need to check each VIP individually and only remove IPv6 VIPs
|
||||
for i := range cluster.Network {
|
||||
if utils.IsIPv6(cluster.Network[i].IP()) {
|
||||
log.Info("Removing IPv6 VIP immediately (required to prevent DAD failures on new leader)", "ip", cluster.Network[i].IP())
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("delete VIP", "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
} else {
|
||||
log.Info("Preserving IPv4 VIP address on interface, only stopped ARP broadcasting", "ip", cluster.Network[i].IP())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy behavior: delete VIP addresses on leadership loss
|
||||
log.Info("Deleting VIP addresses on leadership loss (legacy behavior)")
|
||||
for i := range cluster.Network {
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("delete VIP", "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Fatal("lost leadership, restarting kube-vip")
|
||||
log.Error("lost leadership, restarting kube-vip")
|
||||
panic("") // TODO - we could also return here
|
||||
},
|
||||
onNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
log.Infof("Node [%s] is assuming leadership of the cluster", identity)
|
||||
log.Info("New leader", "leader", identity)
|
||||
|
||||
// If we're not the new leader and we have VIPs preserved from previous leadership,
|
||||
// we need to clean them up to avoid conflicts.
|
||||
if identity != c.NodeName && c.PreserveVIPOnLeadershipLoss {
|
||||
log.Info("Cleaning up preserved VIPs as another node became leader", "new_leader", identity)
|
||||
for i := range cluster.Network {
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("failed to cleanup preserved VIP", "vip", cluster.Network[i].IP(), "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("cleaned up preserved VIP to avoid conflict", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface(), "new_leader", identity)
|
||||
} else {
|
||||
log.Debug("VIP was not present on this node", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -290,17 +345,17 @@ func (cluster *Cluster) runEtcdLeaderElectionOrDie(ctx context.Context, run *run
|
||||
})
|
||||
}
|
||||
|
||||
func (sm *Manager) NodeWatcher(lb *loadbalancer.IPVSLoadBalancer, port int) error {
|
||||
func (sm *Manager) NodeWatcher(ctxArp context.Context, lb *loadbalancer.IPVSLoadBalancer, port uint16) error {
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
log.Infof("Kube-Vip is watching nodes for control-plane labels")
|
||||
log.Info("Kube-Vip is watching nodes for control-plane labels")
|
||||
|
||||
listOptions := metav1.ListOptions{
|
||||
LabelSelector: "node-role.kubernetes.io/control-plane",
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcher("1", &cache.ListWatch{
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.KubernetesClient.CoreV1().Nodes().Watch(context.Background(), listOptions)
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctxArp, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.RetryWatcherClient.CoreV1().Nodes().Watch(context.Background(), listOptions)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -328,9 +383,16 @@ func (sm *Manager) NodeWatcher(lb *loadbalancer.IPVSLoadBalancer, port int) erro
|
||||
// Find the node IP address (this isn't foolproof)
|
||||
for x := range node.Status.Addresses {
|
||||
if node.Status.Addresses[x].Type == v1.NodeInternalIP {
|
||||
err = lb.AddBackend(node.Status.Addresses[x].Address, port)
|
||||
if err != nil {
|
||||
log.Errorf("add IPVS backend [%v]", err)
|
||||
if checkIfNodeIsReady(node) {
|
||||
err = lb.AddBackend(node.Status.Addresses[x].Address, port)
|
||||
if err != nil {
|
||||
log.Error("add IPVS backend", "err", err)
|
||||
}
|
||||
} else {
|
||||
err = lb.RemoveBackend(node.Status.Addresses[x].Address, port)
|
||||
if err != nil {
|
||||
log.Error("remove IPVS backend", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,12 +407,12 @@ func (sm *Manager) NodeWatcher(lb *loadbalancer.IPVSLoadBalancer, port int) erro
|
||||
if node.Status.Addresses[x].Type == v1.NodeInternalIP {
|
||||
err = lb.RemoveBackend(node.Status.Addresses[x].Address, port)
|
||||
if err != nil {
|
||||
log.Errorf("Del IPVS backend [%v]", err)
|
||||
log.Error("Del IPVS backend", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Node [%s] has been deleted", node.Name)
|
||||
log.Info("Node deleted", "name", node.Name)
|
||||
|
||||
case watch.Bookmark:
|
||||
// Un-used
|
||||
@@ -361,15 +423,29 @@ func (sm *Manager) NodeWatcher(lb *loadbalancer.IPVSLoadBalancer, port int) erro
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, ok := errObject.(*apierrors.StatusError)
|
||||
if !ok {
|
||||
log.Errorf(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
|
||||
log.Error(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
|
||||
}
|
||||
|
||||
status := statusErr.ErrStatus
|
||||
log.Errorf("%v", status)
|
||||
log.Error("watcher", "status", status)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
log.Infoln("Exiting Node watcher")
|
||||
log.Info("Exiting Node watcher")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIfNodeIsReady(node *v1.Node) bool {
|
||||
if node == nil {
|
||||
return false
|
||||
}
|
||||
for _, condition := range node.Status.Conditions {
|
||||
if condition.Type == v1.NodeReady {
|
||||
if condition.Status == v1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,26 +2,33 @@ package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/backend"
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/equinixmetal"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/loadbalancer"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
"github.com/packethost/packngo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vishvananda/netlink"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Config, sm *Manager, bgpServer *bgp.Server, packetClient *packngo.Client) error {
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Config, sm *Manager, bgpServer *bgp.Server, cancelLeaderElection context.CancelFunc) error {
|
||||
var err error
|
||||
|
||||
// listen for interrupts or the Linux SIGTERM signal and cancel
|
||||
// our context, which the leader election code will observe and
|
||||
@@ -33,188 +40,283 @@ func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Co
|
||||
// Add Notification for SIGTERM (sent from Kubernetes)
|
||||
signal.Notify(signalChan, syscall.SIGTERM)
|
||||
|
||||
for i := range cluster.Network {
|
||||
loadbalancers := []*loadbalancer.IPVSLoadBalancer{}
|
||||
|
||||
if cluster.Network[i].IsDDNS() {
|
||||
if err := cluster.StartDDNS(ctxDNS); err != nil {
|
||||
log.Error(err)
|
||||
var arpWG sync.WaitGroup
|
||||
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
|
||||
if network.IsDDNS() {
|
||||
if err := cluster.StartDDNS(ctxDNS, cluster.Network[i]); err != nil {
|
||||
log.Error("failed to start DDNS", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := network.SetMask(c.VIPSubnet); err != nil {
|
||||
log.Error("failed to set mask", "subnet", c.VIPSubnet, "err", err)
|
||||
panic("")
|
||||
}
|
||||
|
||||
// start the dns updater if address is dns
|
||||
if cluster.Network[i].IsDNS() {
|
||||
log.Infof("starting the DNS updater for the address %s", cluster.Network[i].DNSName())
|
||||
ipUpdater := vip.NewIPUpdater(cluster.Network[i])
|
||||
if network.IsDNS() {
|
||||
log.Info("starting the DNS updater", "address", network.DNSName())
|
||||
ipUpdater := vip.NewIPUpdater(network)
|
||||
ipUpdater.Run(ctxDNS)
|
||||
}
|
||||
|
||||
err = cluster.Network[i].AddIP()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
if c.EnableMetal {
|
||||
// We're not using Equinix Metal with BGP
|
||||
if !c.EnableBGP {
|
||||
// Attempt to attach the EIP in the standard manner
|
||||
log.Debugf("Attaching the Equinix Metal EIP through the API to this host")
|
||||
err = equinixmetal.AttachEIP(packetClient, c, id)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if !c.EnableRoutingTable {
|
||||
// Normal VIP addition, use skipDAD=false for normal DAD process
|
||||
if _, err = network.AddIP(false, false); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if c.EnableBGP {
|
||||
// Lets advertise the VIP over BGP, the host needs to be passed using CIDR notation
|
||||
cidrVip := fmt.Sprintf("%s/%s", cluster.Network[i].IP(), c.VIPCIDR)
|
||||
log.Debugf("Attempting to advertise the address [%s] over BGP", cidrVip)
|
||||
|
||||
err = bgpServer.AddHost(cidrVip)
|
||||
log.Debug("Attempting to advertise over BGP", "address", network.CIDR())
|
||||
err = bgpServer.AddHost(network.CIDR())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if c.EnableLoadBalancer {
|
||||
|
||||
log.Infof("Starting IPVS LoadBalancer")
|
||||
|
||||
lb, err := loadbalancer.NewIPVSLB(cluster.Network[i].IP(), c.LoadBalancerPort, c.LoadBalancerForwardingMethod)
|
||||
lb, err := loadbalancer.NewIPVSLB(network.IP(), c.LoadBalancerPort, c.LoadBalancerForwardingMethod, c.BackendHealthCheckInterval, c.Interface, cancelLeaderElection, signalChan)
|
||||
if err != nil {
|
||||
log.Errorf("Error creating IPVS LoadBalancer [%s]", err)
|
||||
log.Error("Error creating IPVS LoadBalancer", "err", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
err = sm.NodeWatcher(lb, c.Port)
|
||||
err = sm.NodeWatcher(ctxArp, lb, c.Port) //TODO: We're using the ctxARP as the context this will change when rkatz finishes his change
|
||||
if err != nil {
|
||||
log.Errorf("Error watching node labels [%s]", err)
|
||||
log.Error("Error watching node labels", "err", err)
|
||||
}
|
||||
}()
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-signalChan
|
||||
err = lb.RemoveIPVSLB()
|
||||
if err != nil {
|
||||
log.Errorf("Error stopping IPVS LoadBalancer [%s]", err)
|
||||
}
|
||||
log.Info("Stopping IPVS LoadBalancer")
|
||||
}()
|
||||
|
||||
loadbalancers = append(loadbalancers, lb)
|
||||
}
|
||||
|
||||
if c.EnableARP {
|
||||
// ctxArp, cancelArp = context.WithCancel(context.Background())
|
||||
|
||||
go func(ctx context.Context) {
|
||||
ipString := cluster.Network[i].IP()
|
||||
isIPv6 := vip.IsIPv6(ipString)
|
||||
|
||||
var ndp *vip.NdpResponder
|
||||
if isIPv6 {
|
||||
ndp, err = vip.NewNDPResponder(c.Interface)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create new NDP Responder")
|
||||
}
|
||||
}
|
||||
|
||||
if ndp != nil {
|
||||
defer ndp.Close()
|
||||
}
|
||||
log.Infof("Gratuitous Arp broadcast will repeat every 3 seconds for [%s]", ipString)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done(): // if cancel() execute
|
||||
return
|
||||
default:
|
||||
cluster.ensureIPAndSendGratuitous(c.Interface, ndp)
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}(ctxArp)
|
||||
arpWG.Add(1)
|
||||
go cluster.layer2Update(ctxArp, network, c, &arpWG)
|
||||
}
|
||||
}
|
||||
|
||||
if c.EnableRoutingTable {
|
||||
err = cluster.Network[i].AddRoute()
|
||||
if c.EnableLoadBalancer {
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
<-signalChan
|
||||
for _, lb := range loadbalancers {
|
||||
err = lb.RemoveIPVSLB()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
log.Error("Error stopping IPVS LoadBalancer", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.EnableRoutingTable {
|
||||
backendMapV4 := backend.Map{}
|
||||
backendMapV6 := backend.Map{}
|
||||
// only check localhost
|
||||
|
||||
nodename := ""
|
||||
if c.NodeName != "" {
|
||||
nodename = c.NodeName
|
||||
} else {
|
||||
nodename = os.Getenv("HOSTNAME")
|
||||
}
|
||||
|
||||
ips := []string{}
|
||||
if nodename != "" {
|
||||
if ips, err = getNodeIPs(ctxArp, nodename, sm.KubernetesClient); err != nil && !apierrors.IsNotFound(err) {
|
||||
log.Error("failed to get IP of control-plane nod", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
isV6, err := isV6(cluster.Network[0].IP())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse IP '%s'", cluster.Network[0].IP())
|
||||
}
|
||||
if !isV6 {
|
||||
ips = append(ips, "127.0.0.1")
|
||||
} else {
|
||||
ips = append(ips, "::1")
|
||||
}
|
||||
|
||||
log.Info("no IP address found for node - will fallback to use localhost address", "addresses", ips)
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
entry := backend.Entry{Addr: ip, Port: c.Port}
|
||||
ipv6, err := isV6(ip)
|
||||
if err != nil {
|
||||
log.Error("failed to check IP type", "IP", ip, "error", err)
|
||||
}
|
||||
if !ipv6 {
|
||||
backendMapV4[entry] = false
|
||||
} else {
|
||||
backendMapV6[entry] = false
|
||||
}
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
|
||||
// will wait for system interrupt and will send stop signal to backend watch
|
||||
go func() {
|
||||
<-signalChan
|
||||
stop <- struct{}{}
|
||||
}()
|
||||
|
||||
backend.Watch(func() {
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
networkIP := network.IP()
|
||||
isNetworkV6, err := isV6(networkIP)
|
||||
if err != nil {
|
||||
log.Error("failed to check IP type", "IP", networkIP, "error", err)
|
||||
continue
|
||||
}
|
||||
log.Debug("current ip to process", "ip", networkIP)
|
||||
|
||||
backendMap := &backendMapV4
|
||||
if isNetworkV6 {
|
||||
backendMap = &backendMapV6
|
||||
}
|
||||
|
||||
for entry := range *backendMap {
|
||||
log.Debug("entry.Check() for entry", "entry", entry)
|
||||
if entry.Check() {
|
||||
log.Debug("entry.Check() true")
|
||||
// Normal VIP addition with precheck, use skipDAD=false for normal DAD process
|
||||
_, err = network.AddIP(true, false)
|
||||
if err != nil {
|
||||
log.Error("error adding address", "err", err)
|
||||
}
|
||||
if !(*backendMap)[entry] {
|
||||
log.Info("added backend", "ip", network.IP())
|
||||
}
|
||||
|
||||
err = network.AddRoute(true)
|
||||
if err != nil && !errors.Is(err, fs.ErrExist) && !errors.Is(err, syscall.ESRCH) {
|
||||
log.Warn(err.Error())
|
||||
} else if err == nil && !(*backendMap)[entry] {
|
||||
log.Info("added route", "route", network.PrepareRoute().String())
|
||||
}
|
||||
|
||||
(*backendMap)[entry] = true
|
||||
break
|
||||
}
|
||||
(*backendMap)[entry] = false
|
||||
}
|
||||
|
||||
deleteAddress := true
|
||||
for entry := range *backendMap {
|
||||
if (*backendMap)[entry] {
|
||||
deleteAddress = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if deleteAddress {
|
||||
err = network.DeleteRoute()
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, syscall.ESRCH) {
|
||||
log.Warn("deleting route", "err", err)
|
||||
} else if err == nil {
|
||||
log.Info("deleted route", "route", network.PrepareRoute().String())
|
||||
}
|
||||
|
||||
deleted, err := network.DeleteIP()
|
||||
if err != nil {
|
||||
log.Error("error deleting IP", "err", err)
|
||||
panic("")
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", network.IP(), "interface", network.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
}, c.BackendHealthCheckInterval, stop)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isV6(ip string) (bool, error) {
|
||||
ipaddr := net.ParseIP(ip)
|
||||
if ipaddr == nil {
|
||||
return false, fmt.Errorf("failed to parse IP '%s'", ip)
|
||||
}
|
||||
return ipaddr.To4() == nil, nil
|
||||
}
|
||||
|
||||
func getNodeIPs(ctx context.Context, nodename string, client *kubernetes.Clientset) ([]string, error) {
|
||||
node, err := client.CoreV1().Nodes().Get(ctx, nodename, metav1.GetOptions{})
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return []string{}, fmt.Errorf("failed to get data about '%s' node: %w", nodename, err)
|
||||
}
|
||||
ips := []string{}
|
||||
for _, addr := range node.Status.Addresses {
|
||||
if addr.Type == corev1.NodeInternalIP {
|
||||
ips = append(ips, addr.Address)
|
||||
}
|
||||
}
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
// StartLoadBalancerService will start a VIP instance and leave it for kube-proxy to handle
|
||||
func (cluster *Cluster) StartLoadBalancerService(c *kubevip.Config, bgp *bgp.Server) {
|
||||
func (cluster *Cluster) StartLoadBalancerService(ctx context.Context, c *kubevip.Config, bgp *bgp.Server, name string, CountRouteReferences func(*netlink.Route) int) {
|
||||
// use a Go context so we can tell the arp loop code when we
|
||||
// want to step down
|
||||
//nolint
|
||||
ctxArp, cancelArp := context.WithCancel(context.Background())
|
||||
ctxArp, cancelArp := context.WithCancel(ctx)
|
||||
|
||||
cluster.stop = make(chan bool, 1)
|
||||
cluster.completed = make(chan bool, 1)
|
||||
|
||||
var arpWG sync.WaitGroup
|
||||
|
||||
log.Debug("StartLoadBalancerService")
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
|
||||
err := network.DeleteIP()
|
||||
if err != nil {
|
||||
log.Warnf("Attempted to clean existing VIP => %v", err)
|
||||
log.Debug("current ip to process", "ip", network.IP(), "mask", c.VIPSubnet)
|
||||
if err := network.SetMask(c.VIPSubnet); err != nil {
|
||||
log.Error("failed to set mask", "subnet", c.VIPSubnet, "err", err)
|
||||
panic("")
|
||||
}
|
||||
_, err := network.DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("attempted to clean existing VIP", "err", err)
|
||||
}
|
||||
log.Debug("config flags", "enable_routing_table", c.EnableRoutingTable, "enable_leader_election", c.EnableLeaderElection, "enable_services_election", c.EnableServicesElection)
|
||||
|
||||
if c.EnableRoutingTable && (c.EnableLeaderElection || c.EnableServicesElection) {
|
||||
err = network.AddRoute()
|
||||
err = network.AddRoute(false)
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
} else if !c.EnableRoutingTable {
|
||||
err = network.AddIP()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
log.Warn(err.Error())
|
||||
} else {
|
||||
log.Info("successful add Route")
|
||||
}
|
||||
}
|
||||
|
||||
// Normal VIP addition, use skipDAD=false for normal DAD process
|
||||
if _, err = network.AddIP(false, false); err != nil {
|
||||
log.Warn(err.Error())
|
||||
} else {
|
||||
log.Info("successful add IP")
|
||||
}
|
||||
|
||||
if c.EnableARP {
|
||||
// ctxArp, cancelArp = context.WithCancel(context.Background())
|
||||
|
||||
ipString := network.IP()
|
||||
|
||||
var ndp *vip.NdpResponder
|
||||
if vip.IsIPv6(ipString) {
|
||||
ndp, err = vip.NewNDPResponder(c.Interface)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create new NDP Responder")
|
||||
}
|
||||
}
|
||||
go func(ctx context.Context) {
|
||||
if ndp != nil {
|
||||
defer ndp.Close()
|
||||
}
|
||||
log.Debugf("(svcs) broadcasting ARP update for %s via %s, every %dms", ipString, c.Interface, c.ArpBroadcastRate)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done(): // if cancel() execute
|
||||
log.Debugf("(svcs) ending ARP update for %s via %s, every %dms", ipString, c.Interface, c.ArpBroadcastRate)
|
||||
return
|
||||
default:
|
||||
cluster.ensureIPAndSendGratuitous(c.Interface, ndp)
|
||||
}
|
||||
if c.ArpBroadcastRate < 500 {
|
||||
log.Errorf("arp broadcast rate is [%d], this shouldn't be lower that 300ms (defaulting to 3000)", c.ArpBroadcastRate)
|
||||
c.ArpBroadcastRate = 3000
|
||||
}
|
||||
time.Sleep(time.Duration(c.ArpBroadcastRate) * time.Millisecond)
|
||||
}
|
||||
}(ctxArp)
|
||||
arpWG.Add(1)
|
||||
go cluster.layer2Update(ctxArp, network, c, &arpWG)
|
||||
}
|
||||
|
||||
if c.EnableBGP && (c.EnableLeaderElection || c.EnableServicesElection) {
|
||||
// Lets advertise the VIP over BGP, the host needs to be passed using CIDR notation
|
||||
cidrVip := fmt.Sprintf("%s/%s", network.IP(), c.VIPCIDR)
|
||||
log.Debugf("(svcs) attempting to advertise the address [%s] over BGP", cidrVip)
|
||||
err = bgp.AddHost(cidrVip)
|
||||
log.Debug("(svcs) attempting to advertise over BGP", "address", network.CIDR())
|
||||
err = bgp.AddHost(network.CIDR())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,23 +326,57 @@ func (cluster *Cluster) StartLoadBalancerService(c *kubevip.Config, bgp *bgp.Ser
|
||||
// Stop the Arp context if it is running
|
||||
cancelArp()
|
||||
|
||||
if c.EnableRoutingTable && (c.EnableLeaderElection || c.EnableServicesElection) {
|
||||
arpWG.Wait() // wait for all cluster ARP/NDP to be finished
|
||||
|
||||
log.Info("[LOADBALANCER] Stopping load balancers", "name", name)
|
||||
|
||||
if c.EnableRoutingTable {
|
||||
for i := range cluster.Network {
|
||||
if err := cluster.Network[i].DeleteRoute(); err != nil {
|
||||
log.Warnf("%v", err)
|
||||
// chek if route is not referenced by another service
|
||||
r := cluster.Network[i].PrepareRoute()
|
||||
if CountRouteReferences(r) < 1 {
|
||||
log.Info("[VIP] Deleting Route for VIP", "IP", cluster.Network[i].IP())
|
||||
if err := cluster.Network[i].DeleteRoute(); err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(cluster.completed)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("[LOADBALANCER] Stopping load balancers")
|
||||
|
||||
for i := range cluster.Network {
|
||||
log.Infof("[VIP] Releasing the Virtual IP [%s]", cluster.Network[i].IP())
|
||||
if err := cluster.Network[i].DeleteIP(); err != nil {
|
||||
log.Warnf("%v", err)
|
||||
if c.EnableARP && cluster.arpMgr.Count(cluster.Network[i].ARPName()) > 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle VIP cleanup based on configuration
|
||||
if c.PreserveVIPOnLeadershipLoss {
|
||||
// For IPv6, we must remove VIPs immediately to avoid DAD failures on the new leader
|
||||
// IPv6 Duplicate Address Detection will fail if the new leader tries to add an IP
|
||||
// that is still present on this node's interface
|
||||
if utils.IsIPv6(cluster.Network[i].IP()) {
|
||||
log.Info("[VIP] Removing IPv6 VIP immediately (required to prevent DAD failures on new leader)", "ip", cluster.Network[i].IP())
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
} else {
|
||||
log.Info("[VIP] Preserving IPv4 VIP address on interface, only stopped ARP broadcasting", "ip", cluster.Network[i].IP())
|
||||
}
|
||||
} else {
|
||||
// Legacy behavior: delete VIP addresses on leadership loss
|
||||
log.Info("[VIP] Deleting VIP", "ip", cluster.Network[i].IP())
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,48 +384,35 @@ func (cluster *Cluster) StartLoadBalancerService(c *kubevip.Config, bgp *bgp.Ser
|
||||
}()
|
||||
}
|
||||
|
||||
// ensureIPAndSendGratuitous - adds IP to the interface if missing, and send
|
||||
// either a gratuitous ARP or gratuitous NDP. Re-adds the interface if it is IPv6
|
||||
// and in a dadfailed state.
|
||||
func (cluster *Cluster) ensureIPAndSendGratuitous(iface string, ndp *vip.NdpResponder) {
|
||||
for i := range cluster.Network {
|
||||
ipString := cluster.Network[i].IP()
|
||||
isIPv6 := vip.IsIPv6(ipString)
|
||||
// Check if IP is dadfailed
|
||||
if cluster.Network[i].IsDADFAIL() {
|
||||
log.Warnf("IP address is in dadfailed state, removing [%s] from interface [%s]", ipString, iface)
|
||||
err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
}
|
||||
// Layer2Update, handles the creation of the
|
||||
func (cluster *Cluster) layer2Update(ctx context.Context, network vip.Network, c *kubevip.Config, arpWG *sync.WaitGroup) {
|
||||
defer arpWG.Done()
|
||||
log.Info("layer 2 broadcaster starting")
|
||||
var ndp *vip.NdpResponder
|
||||
var err error
|
||||
ipString := network.IP()
|
||||
if utils.IsIPv6(ipString) {
|
||||
if network.IPisLinkLocal() {
|
||||
log.Error("layer2 is link-local can't use NDP", "address", ipString)
|
||||
|
||||
// Ensure the address exists on the interface before attempting to ARP
|
||||
set, err := cluster.Network[i].IsSet()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
if !set {
|
||||
log.Warnf("Re-applying the VIP configuration [%s] to the interface [%s]", ipString, iface)
|
||||
err = cluster.Network[i].AddIP()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if isIPv6 {
|
||||
// Gratuitous NDP, will broadcast new MAC <-> IPv6 address
|
||||
err := ndp.SendGratuitous(ipString)
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
}
|
||||
} else {
|
||||
// Gratuitous ARP, will broadcast to new MAC <-> IPv4 address
|
||||
err := vip.ARPSendGratuitous(ipString, iface)
|
||||
ndp, err = vip.NewNDPResponder(network.Interface())
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
log.Error("failed to create new NDP Responder", "error", err)
|
||||
} else {
|
||||
if ndp != nil {
|
||||
defer ndp.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("layer 2 update", "ip", ipString, "interface", network.Interface(), "ms", c.ArpBroadcastRate)
|
||||
|
||||
arpInstance := arp.NewInstance(network, ndp)
|
||||
cluster.arpMgr.Insert(arpInstance)
|
||||
|
||||
<-ctx.Done() // if cancel() execute
|
||||
log.Debug("ending layer 2 update", "ip", ipString, "interface", network.Interface(), "ms", c.ArpBroadcastRate)
|
||||
cluster.arpMgr.RemoveOnLeadershipLoss(arpInstance)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ package cluster
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/packethost/packngo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
@@ -16,7 +15,7 @@ func (cluster *Cluster) StartSingleNode(c *kubevip.Config, disableVIP bool) erro
|
||||
// Start kube-vip as a single node server
|
||||
|
||||
// TODO - Split all this code out as a separate function
|
||||
log.Infoln("Starting kube-vip as a single node cluster")
|
||||
log.Info("Starting kube-vip as a single node cluster")
|
||||
|
||||
log.Info("This node is assuming leadership of the cluster")
|
||||
|
||||
@@ -25,14 +24,18 @@ func (cluster *Cluster) StartSingleNode(c *kubevip.Config, disableVIP bool) erro
|
||||
|
||||
for i := range cluster.Network {
|
||||
if !disableVIP {
|
||||
err := cluster.Network[i].DeleteIP()
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warnf("Attempted to clean existing VIP => %v", err)
|
||||
log.Warn("Attempted to clean existing VIP", "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
|
||||
err = cluster.Network[i].AddIP()
|
||||
// Normal VIP addition for single node, use skipDAD=false for normal DAD process
|
||||
_, err = cluster.Network[i].AddIP(false, false)
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,7 +44,7 @@ func (cluster *Cluster) StartSingleNode(c *kubevip.Config, disableVIP bool) erro
|
||||
// Gratuitous ARP, will broadcast to new MAC <-> IP
|
||||
err := vip.ARPSendGratuitous(cluster.Network[i].IP(), c.Interface)
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,20 +54,23 @@ func (cluster *Cluster) StartSingleNode(c *kubevip.Config, disableVIP bool) erro
|
||||
|
||||
if !disableVIP {
|
||||
for i := range cluster.Network {
|
||||
log.Infof("[VIP] Releasing the Virtual IP [%s]", cluster.Network[i].IP())
|
||||
err := cluster.Network[i].DeleteIP()
|
||||
log.Info("[VIP] Releasing the VIP", "address", cluster.Network[i].IP())
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warnf("%v", err)
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
close(cluster.completed)
|
||||
}()
|
||||
log.Infoln("Started Load Balancer and Virtual IP")
|
||||
log.Info("Started Load Balancer and Virtual IP")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cluster *Cluster) StartVipService(c *kubevip.Config, sm *Manager, bgp *bgp.Server, packetClient *packngo.Client) error {
|
||||
func (cluster *Cluster) StartVipService(c *kubevip.Config, sm *Manager, bgp *bgp.Server) error {
|
||||
// use a Go context so we can tell the arp loop code when we
|
||||
// want to step down
|
||||
ctxArp, cancelArp := context.WithCancel(context.Background())
|
||||
@@ -75,5 +81,5 @@ func (cluster *Cluster) StartVipService(c *kubevip.Config, sm *Manager, bgp *bgp
|
||||
ctxDNS, cancelDNS := context.WithCancel(context.Background())
|
||||
defer cancelDNS()
|
||||
|
||||
return cluster.vipService(ctxArp, ctxDNS, c, sm, bgp, packetClient)
|
||||
return cluster.vipService(ctxArp, ctxDNS, c, sm, bgp, nil)
|
||||
}
|
||||
|
||||
100
pkg/egress/egress.go
Normal file
100
pkg/egress/egress.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package egress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/iptables"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/nftables"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
func Teardown(podIP, vipIP, namespace, serviceUUID string, annotations map[string]string, useNftables bool) error {
|
||||
// Look up the destination ports from the annotations on the service
|
||||
destinationPorts := annotations[kubevip.EgressDestinationPorts]
|
||||
deniedNetworks := annotations[kubevip.EgressDeniedNetworks]
|
||||
allowedNetworks := annotations[kubevip.EgressAllowedNetworks]
|
||||
internalEgress := annotations[kubevip.EgressInternal]
|
||||
|
||||
protocol := iptables.ProtocolIPv4
|
||||
IPv6 := false
|
||||
if utils.IsIPv6(podIP) {
|
||||
protocol = iptables.ProtocolIPv6
|
||||
IPv6 = true
|
||||
}
|
||||
|
||||
// Use the internal egress implementation
|
||||
if internalEgress != "" {
|
||||
return nftables.DeleteSNAT(IPv6, serviceUUID)
|
||||
}
|
||||
|
||||
i, err := vip.CreateIptablesClient(useNftables, namespace, protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error Creating iptables client [%s]", err)
|
||||
}
|
||||
|
||||
if deniedNetworks != "" {
|
||||
networks := strings.Split(deniedNetworks, ",")
|
||||
for x := range networks {
|
||||
err = i.DeleteMangleReturnForNetwork(vip.MangleChainName, networks[x])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting rules in mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowedNetworks != "" {
|
||||
networks := strings.Split(allowedNetworks, ",")
|
||||
for x := range networks {
|
||||
err = i.DeleteMangleMarkingForNetwork(podIP, vip.MangleChainName, networks[x])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting rules in mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove the marking of egress packets
|
||||
err = i.DeleteMangleMarking(podIP, vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear up SNAT rules
|
||||
if destinationPorts != "" {
|
||||
fixedPorts := strings.Split(destinationPorts, ",")
|
||||
|
||||
for _, fixedPort := range fixedPorts {
|
||||
var proto, port string
|
||||
|
||||
data := strings.Split(fixedPort, ":")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
} else if len(data) == 1 {
|
||||
proto = "tcp"
|
||||
port = data[0]
|
||||
} else {
|
||||
proto = data[0]
|
||||
port = data[1]
|
||||
}
|
||||
|
||||
err = i.DeleteSourceNatForDestinationPort(podIP, vipIP, port, proto)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
err = i.DeleteSourceNat(podIP, vipIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = vip.DeleteExistingSessions(podIP, false, destinationPorts, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
165
pkg/endpoints/endpoints.go
Normal file
165
pkg/endpoints/endpoints.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
config *kubevip.Config
|
||||
provider providers.Provider
|
||||
bgpServer *bgp.Server
|
||||
worker endpointWorker
|
||||
instances *[]*instance.Instance
|
||||
}
|
||||
|
||||
func NewEndpointProcessor(config *kubevip.Config, provider providers.Provider, bgpServer *bgp.Server,
|
||||
instances *[]*instance.Instance) *Processor {
|
||||
return &Processor{
|
||||
config: config,
|
||||
provider: provider,
|
||||
bgpServer: bgpServer,
|
||||
instances: instances,
|
||||
worker: newEndpointWorker(config, provider, bgpServer, instances),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) AddOrModify(ctx *servicecontext.Context, event watch.Event,
|
||||
lastKnownGoodEndpoint *string, service *v1.Service, id string, leaderElectionActive *bool,
|
||||
serviceFunc func(context.Context, *v1.Service) error,
|
||||
leaderCtx *context.Context, cancel *context.CancelFunc) (bool, error) {
|
||||
|
||||
var err error
|
||||
if err = p.provider.LoadObject(event.Object, *cancel); err != nil {
|
||||
return false, fmt.Errorf("[%s] error loading k8s object: %w", p.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
endpoints, err := p.worker.getEndpoints(service, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := p.worker.setInstanceEndpointsStatus(service, endpoints); err != nil {
|
||||
log.Error("updating instance", "err", err)
|
||||
}
|
||||
|
||||
// Find out if we have any local endpoints
|
||||
// if out endpoint is empty then populate it
|
||||
// if not, go through the endpoints and see if ours still exists
|
||||
// If we have a local endpoint then begin the leader Election, unless it's already running
|
||||
//
|
||||
|
||||
// Check that we have local endpoints
|
||||
if len(endpoints) != 0 {
|
||||
// Ignore IPv4
|
||||
if service.Annotations[kubevip.EgressIPv6] == "true" && net.ParseIP(endpoints[0]).To4() != nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
p.updateLastKnownGoodEndpoint(lastKnownGoodEndpoint, endpoints, service, leaderElectionActive, *cancel)
|
||||
// start leader election if it's enabled and not already started
|
||||
if !*leaderElectionActive && p.config.EnableServicesElection {
|
||||
go func() {
|
||||
*leaderCtx, *cancel = context.WithCancel(ctx.Ctx)
|
||||
startLeaderElection(*leaderCtx, leaderElectionActive, service, serviceFunc)
|
||||
}()
|
||||
}
|
||||
|
||||
// There are local endpoints available on the node
|
||||
if !p.config.EnableServicesElection && !p.config.EnableLeaderElection {
|
||||
if err := p.worker.processInstance(ctx, service, leaderElectionActive); err != nil {
|
||||
return false, fmt.Errorf("failed to process non-empty instance: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There are no local endpoints
|
||||
p.worker.clear(ctx, lastKnownGoodEndpoint, service, *cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
// Set the service accordingly
|
||||
p.updateAnnotations(service, lastKnownGoodEndpoint)
|
||||
|
||||
log.Debug("watcher", "provider",
|
||||
p.provider.GetLabel(), "service name", service.Name, "namespace", service.Namespace, "endpoints", len(endpoints), "last endpoint", *lastKnownGoodEndpoint, "active leader election", *leaderElectionActive)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *Processor) Delete(service *v1.Service, id string) error {
|
||||
if err := p.worker.delete(service, id); err != nil {
|
||||
return fmt.Errorf("[%s] error deleting service: %w", p.provider.GetLabel(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) updateLastKnownGoodEndpoint(lastKnownGoodEndpoint *string, endpoints []string, service *v1.Service, leaderElectionActive *bool, cancel context.CancelFunc) {
|
||||
// if we haven't populated one, then do so
|
||||
if *lastKnownGoodEndpoint == "" {
|
||||
*lastKnownGoodEndpoint = endpoints[0]
|
||||
return
|
||||
}
|
||||
|
||||
// check out previous endpoint exists
|
||||
stillExists := false
|
||||
|
||||
for x := range endpoints {
|
||||
if endpoints[x] == *lastKnownGoodEndpoint {
|
||||
stillExists = true
|
||||
}
|
||||
}
|
||||
// If the last endpoint no longer exists, we cancel our leader Election, and set another endpoint as last known good
|
||||
if !stillExists {
|
||||
p.worker.removeEgress(service, lastKnownGoodEndpoint)
|
||||
if *leaderElectionActive && (p.config.EnableServicesElection || p.config.EnableLeaderElection) {
|
||||
log.Warn("existing endpoint has been removed, restarting leaderElection", "provider", p.provider.GetLabel(), "endpoint", *lastKnownGoodEndpoint)
|
||||
// Stop the existing leaderElection
|
||||
cancel()
|
||||
// disable last leaderElection flag
|
||||
*leaderElectionActive = false
|
||||
}
|
||||
// Set our active endpoint to an existing one
|
||||
*lastKnownGoodEndpoint = endpoints[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) updateAnnotations(service *v1.Service, lastKnownGoodEndpoint *string) {
|
||||
// Set the service accordingly
|
||||
if service.Annotations[kubevip.Egress] == "true" {
|
||||
activeEndpointAnnotation := kubevip.ActiveEndpoint
|
||||
|
||||
if !p.config.EnableEndpoints && p.provider.GetProtocol() == string(discoveryv1.AddressTypeIPv6) {
|
||||
activeEndpointAnnotation = kubevip.ActiveEndpointIPv6
|
||||
}
|
||||
service.Annotations[activeEndpointAnnotation] = *lastKnownGoodEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
func startLeaderElection(ctx context.Context, leaderElectionActive *bool, service *v1.Service, serviceFunc func(context.Context, *v1.Service) error) {
|
||||
// This is a blocking function, that will restart (in the event of failure)
|
||||
for {
|
||||
// if the context isn't cancelled restart
|
||||
if ctx.Err() != context.Canceled {
|
||||
*leaderElectionActive = true
|
||||
err := serviceFunc(ctx, service)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
*leaderElectionActive = false
|
||||
} else {
|
||||
*leaderElectionActive = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
129
pkg/endpoints/endpoints_bgp.go
Normal file
129
pkg/endpoints/endpoints_bgp.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type BGP struct {
|
||||
generic
|
||||
bgpServer *bgp.Server
|
||||
}
|
||||
|
||||
func newBGP(generic generic, bgpServer *bgp.Server) endpointWorker {
|
||||
return &BGP{
|
||||
generic: generic,
|
||||
bgpServer: bgpServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BGP) processInstance(ctx *servicecontext.Context, service *v1.Service, leaderElectionActive *bool) error {
|
||||
if instance := instance.FindServiceInstance(service, *b.instances); instance != nil {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
if !ctx.IsNetworkConfigured(cluster.Network[i].IP()) {
|
||||
log.Debug("attempting to advertise BGP service", "provider", b.provider.GetLabel(), "ip", cluster.Network[i].IP())
|
||||
err := b.bgpServer.AddHost(cluster.Network[i].CIDR())
|
||||
if err != nil {
|
||||
log.Error("error adding BGP host", "provider", b.provider.GetLabel(), "err", err)
|
||||
} else {
|
||||
log.Info("added BGP host", "provider",
|
||||
b.provider.GetLabel(), "ip", cluster.Network[i].CIDR(), "service name", service.Name, "namespace", service.Namespace)
|
||||
ctx.ConfiguredNetworks.Store(cluster.Network[i].IP(), true)
|
||||
*leaderElectionActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BGP) clear(ctx *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
if !b.config.EnableServicesElection && !b.config.EnableLeaderElection {
|
||||
// If BGP mode is enabled - routes should be deleted
|
||||
if instance := instance.FindServiceInstance(service, *b.instances); instance != nil {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
err := b.bgpServer.DelHost(cluster.Network[i].CIDR())
|
||||
if err != nil {
|
||||
log.Error("deleting BGP host", "provider", b.provider.GetLabel(), "ip", cluster.Network[i].IP(), "err", err)
|
||||
} else {
|
||||
log.Info("deleted BGP host", "provider",
|
||||
b.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace", service.Namespace)
|
||||
ctx.ConfiguredNetworks.Delete(cluster.Network[i].IP())
|
||||
*leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
b.clearEgress(lastKnownGoodEndpoint, service, cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
func (b *BGP) getEndpoints(service *v1.Service, id string) ([]string, error) {
|
||||
return b.getAllEndpoints(service, id)
|
||||
}
|
||||
|
||||
func (b *BGP) delete(service *v1.Service, id string) error {
|
||||
// When no-leader-elecition mode
|
||||
if !b.config.EnableServicesElection && !b.config.EnableLeaderElection {
|
||||
// find all existing local endpoints
|
||||
endpoints, err := b.getEndpoints(service, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error getting endpoints: %w", b.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
// If there were local endpoints deleted
|
||||
if len(endpoints) > 0 {
|
||||
b.deleteAction(service)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BGP) deleteAction(service *v1.Service) {
|
||||
b.clearBGPHosts(service)
|
||||
}
|
||||
|
||||
func (b *BGP) clearBGPHosts(service *v1.Service) {
|
||||
ClearBGPHosts(service, b.instances, b.bgpServer)
|
||||
}
|
||||
|
||||
func (b *BGP) setInstanceEndpointsStatus(_ *v1.Service, _ []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearBGPHosts(service *v1.Service, instances *[]*instance.Instance, bgpServer *bgp.Server) {
|
||||
if instance := instance.FindServiceInstance(service, *instances); instance != nil {
|
||||
ClearBGPHostsByInstance(instance, bgpServer)
|
||||
}
|
||||
}
|
||||
|
||||
func ClearBGPHostsByInstance(instance *instance.Instance, bgpServer *bgp.Server) {
|
||||
if instance == nil {
|
||||
log.Error("failed to clear BGP host for nil instance")
|
||||
return
|
||||
}
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
err := bgpServer.DelHost(network.CIDR())
|
||||
if err != nil {
|
||||
log.Error("[endpoint] error deleting BGP host", "err", err)
|
||||
} else {
|
||||
log.Debug("[endpoint] deleted BGP host", "ip",
|
||||
network.CIDR(), "service name", instance.ServiceSnapshot.Name, "namespace", instance.ServiceSnapshot.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
pkg/endpoints/endpoints_generic.go
Normal file
118
pkg/endpoints/endpoints_generic.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/egress"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type endpointWorker interface {
|
||||
processInstance(svcCtx *servicecontext.Context, service *v1.Service, leaderElectionActive *bool) error
|
||||
clear(svcCtx *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool)
|
||||
getEndpoints(service *v1.Service, id string) ([]string, error)
|
||||
removeEgress(service *v1.Service, lastKnownGoodEndpoint *string)
|
||||
delete(service *v1.Service, id string) error
|
||||
setInstanceEndpointsStatus(service *v1.Service, endpoints []string) error
|
||||
}
|
||||
|
||||
func newEndpointWorker(config *kubevip.Config, provider providers.Provider, bgpServer *bgp.Server, instances *[]*instance.Instance) endpointWorker {
|
||||
generic := newGeneric(config, provider, instances)
|
||||
|
||||
if config.EnableRoutingTable {
|
||||
return newRoutingTable(generic)
|
||||
}
|
||||
if config.EnableBGP {
|
||||
return newBGP(generic, bgpServer)
|
||||
}
|
||||
|
||||
return &generic
|
||||
}
|
||||
|
||||
type generic struct {
|
||||
config *kubevip.Config
|
||||
provider providers.Provider
|
||||
instances *[]*instance.Instance
|
||||
}
|
||||
|
||||
func newGeneric(config *kubevip.Config, provider providers.Provider, instances *[]*instance.Instance) generic {
|
||||
return generic{
|
||||
config: config,
|
||||
provider: provider,
|
||||
instances: instances,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generic) processInstance(_ *servicecontext.Context, _ *v1.Service, _ *bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *generic) clear(_ *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
g.clearEgress(lastKnownGoodEndpoint, service, cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
func (g *generic) clearEgress(lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
if *lastKnownGoodEndpoint != "" {
|
||||
log.Warn("existing endpoint has been removed, no remaining endpoints for leaderElection", "provider", g.provider.GetLabel(), "endpoint", lastKnownGoodEndpoint)
|
||||
if err := egress.Teardown(*lastKnownGoodEndpoint, service.Spec.LoadBalancerIP, service.Namespace, string(service.UID), service.Annotations, g.config.EgressWithNftables); err != nil {
|
||||
log.Error("error removing redundant egress rules", "err", err)
|
||||
}
|
||||
|
||||
*lastKnownGoodEndpoint = "" // reset endpoint
|
||||
if g.config.EnableServicesElection || g.config.EnableLeaderElection {
|
||||
cancel() // stop services watcher
|
||||
}
|
||||
*leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generic) getEndpoints(_ *v1.Service, id string) ([]string, error) {
|
||||
return g.getLocalEndpoints(id)
|
||||
}
|
||||
|
||||
func (g *generic) getLocalEndpoints(id string) ([]string, error) {
|
||||
// Build endpoints
|
||||
var endpoints []string
|
||||
var err error
|
||||
if endpoints, err = g.provider.GetLocalEndpoints(id, g.config); err != nil {
|
||||
return nil, fmt.Errorf("[%s] error getting local endpoints: %w", g.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (g *generic) getAllEndpoints(service *v1.Service, id string) ([]string, error) {
|
||||
// Build endpoints
|
||||
var err error
|
||||
var endpoints []string
|
||||
if !g.config.EnableLeaderElection && !g.config.EnableServicesElection &&
|
||||
service.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
if endpoints, err = g.provider.GetAllEndpoints(); err != nil {
|
||||
return nil, fmt.Errorf("[%s] error getting all endpoints: %w", g.provider.GetLabel(), err)
|
||||
}
|
||||
} else {
|
||||
if endpoints, err = g.provider.GetLocalEndpoints(id, g.config); err != nil {
|
||||
return nil, fmt.Errorf("[%s] error getting local endpoints: %w", g.provider.GetLabel(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (g *generic) removeEgress(_ *v1.Service, _ *string) {
|
||||
}
|
||||
|
||||
func (g *generic) delete(_ *v1.Service, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *generic) setInstanceEndpointsStatus(_ *v1.Service, _ []string) error {
|
||||
return nil
|
||||
}
|
||||
189
pkg/endpoints/endpoints_routing_table.go
Normal file
189
pkg/endpoints/endpoints_routing_table.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/egress"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type RoutingTable struct {
|
||||
generic
|
||||
}
|
||||
|
||||
func newRoutingTable(generic generic) endpointWorker {
|
||||
return &RoutingTable{
|
||||
generic: generic,
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) processInstance(ctx *servicecontext.Context, service *v1.Service, leaderElectionActive *bool) error {
|
||||
instance := instance.FindServiceInstance(service, *rt.instances)
|
||||
if instance != nil {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
if !ctx.IsNetworkConfigured(cluster.Network[i].IP()) && cluster.Network[i].HasEndpoints() {
|
||||
err := cluster.Network[i].AddRoute(false)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EEXIST) {
|
||||
// If route exists, but protocol is not set (e.g. the route was created by the older version
|
||||
// of kube-vip) try to update it if necessary
|
||||
isUpdated, err := cluster.Network[i].UpdateRoutes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error updating existing routes: %w", rt.provider.GetLabel(), err)
|
||||
}
|
||||
if isUpdated {
|
||||
log.Info("updated route", "provider",
|
||||
rt.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace",
|
||||
service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", rt.config.RoutingTableID)
|
||||
} else {
|
||||
log.Info("route already present", "provider",
|
||||
rt.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace",
|
||||
service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", rt.config.RoutingTableID)
|
||||
}
|
||||
} else {
|
||||
// If other error occurs, return error
|
||||
return fmt.Errorf("[%s] error adding route: %s", rt.provider.GetLabel(), err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Info("added route", "provider",
|
||||
rt.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace",
|
||||
service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", rt.config.RoutingTableID)
|
||||
ctx.ConfiguredNetworks.Store(cluster.Network[i].IP(), true)
|
||||
*leaderElectionActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) clear(svcCtx *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
if !rt.config.EnableServicesElection && !rt.config.EnableLeaderElection {
|
||||
if errs := ClearRoutes(service, rt.instances); len(errs) == 0 {
|
||||
svcCtx.ConfiguredNetworks.Clear()
|
||||
} else {
|
||||
for _, err := range errs {
|
||||
log.Error("error while clearing routes", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rt.clearEgress(lastKnownGoodEndpoint, service, cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) getEndpoints(service *v1.Service, id string) ([]string, error) {
|
||||
return rt.getAllEndpoints(service, id)
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) removeEgress(service *v1.Service, lastKnownGoodEndpoint *string) {
|
||||
if err := egress.Teardown(*lastKnownGoodEndpoint, service.Spec.LoadBalancerIP,
|
||||
service.Namespace, string(service.UID), service.Annotations, rt.config.EgressWithNftables); err != nil {
|
||||
log.Warn("removing redundant egress rules", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) delete(service *v1.Service, id string) error {
|
||||
// When no-leader-elecition mode
|
||||
if !rt.config.EnableServicesElection && !rt.config.EnableLeaderElection {
|
||||
// find all existing local endpoints
|
||||
endpoints, err := rt.getEndpoints(service, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error getting endpoints: %w", rt.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
// If there were local endpoints deleted
|
||||
if len(endpoints) > 0 {
|
||||
rt.deleteAction(service)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) deleteAction(service *v1.Service) {
|
||||
ClearRoutes(service, rt.instances)
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) setInstanceEndpointsStatus(service *v1.Service, endpoints []string) error {
|
||||
instance := instance.FindServiceInstanceWithTimeout(service, *rt.instances)
|
||||
if instance == nil {
|
||||
log.Error("failed to find the instance", "namespace", service.Namespace, "name", service.Name, "uid", service.UID, "provider", rt.provider.GetLabel())
|
||||
} else {
|
||||
for _, c := range instance.Clusters {
|
||||
for n := range c.Network {
|
||||
// if there are no endpoints set HasEndpoints false just in case
|
||||
if len(endpoints) < 1 {
|
||||
c.Network[n].SetHasEndpoints(false)
|
||||
}
|
||||
// check if endpoint are available and are of same IP family as service
|
||||
if len(endpoints) > 0 && ((net.ParseIP(c.Network[n].IP()).To4() == nil) == (net.ParseIP(endpoints[0]).To4() == nil)) {
|
||||
c.Network[n].SetHasEndpoints(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearRoutes(service *v1.Service, instances *[]*instance.Instance) []error {
|
||||
errs := []error{}
|
||||
if svcInst := instance.FindServiceInstance(service, *instances); svcInst != nil {
|
||||
clearErrs := ClearRoutesByInstance(service, svcInst, instances)
|
||||
errs = append(errs, clearErrs...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func ClearRoutesByInstance(service *v1.Service, svcInst *instance.Instance, instances *[]*instance.Instance) []error {
|
||||
if svcInst == nil {
|
||||
return []error{fmt.Errorf("failed to remove routes for nil instance of service %s/%s, uid: %s", service.Namespace, service.Name, service.UID)}
|
||||
}
|
||||
errs := []error{}
|
||||
for _, cluster := range svcInst.Clusters {
|
||||
for i := range cluster.Network {
|
||||
route := cluster.Network[i].PrepareRoute()
|
||||
// check if route we are about to delete is not referenced by more than one service
|
||||
if CountRouteReferences(route, instances) <= 1 {
|
||||
err := cluster.Network[i].DeleteRoute()
|
||||
if err != nil && !errors.Is(err, syscall.ESRCH) {
|
||||
log.Error("failed to delete route", "ip", cluster.Network[i].IP(), "err", err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
log.Debug("deleted route", "ip",
|
||||
cluster.Network[i].IP(), "service name", service.Name, "namespace", service.Namespace, "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func CountRouteReferences(route *netlink.Route, instances *[]*instance.Instance) int {
|
||||
cnt := 0
|
||||
for _, instance := range *instances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for n := range cluster.Network {
|
||||
if cluster.Network[n].HasEndpoints() {
|
||||
r := cluster.Network[n].PrepareRoute()
|
||||
if r.Dst.String() == route.Dst.String() {
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cnt
|
||||
}
|
||||
137
pkg/endpoints/providers/endpoints.go
Normal file
137
pkg/endpoints/providers/endpoints.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
log "log/slog"
|
||||
)
|
||||
|
||||
type Endpoints struct {
|
||||
label string
|
||||
//nolint:staticcheck // SA1019 endpoints are moving to an opt-in only
|
||||
endpoints *v1.Endpoints
|
||||
}
|
||||
|
||||
func NewEndpoints() Provider {
|
||||
return &Endpoints{
|
||||
label: "endpoints",
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *Endpoints) CreateRetryWatcher(ctx context.Context, clientSet *kubernetes.Clientset,
|
||||
service *v1.Service) (*watchtools.RetryWatcher, error) {
|
||||
opts := metav1.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector("metadata.name", service.Name).String(),
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctx, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return clientSet.CoreV1().Endpoints(service.Namespace).Watch(ctx, opts)
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating endpoint watcher: %s", err.Error())
|
||||
}
|
||||
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) LoadObject(endpoints runtime.Object, cancel context.CancelFunc) error {
|
||||
//nolint:staticcheck // SA1019 endpoints have to be explicitly requested now
|
||||
eps, ok := endpoints.(*v1.Endpoints)
|
||||
if !ok {
|
||||
cancel()
|
||||
return fmt.Errorf("[%s] unable to parse Kubernetes services from API watcher", ep.GetLabel())
|
||||
}
|
||||
ep.endpoints = eps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetAllEndpoints() ([]string, error) {
|
||||
result := []string{}
|
||||
for subset := range ep.endpoints.Subsets {
|
||||
for address := range ep.endpoints.Subsets[subset].Addresses {
|
||||
addr := strings.Split(ep.endpoints.Subsets[subset].Addresses[address].IP, "/")
|
||||
result = append(result, addr[0])
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetLocalEndpoints(id string, _ *kubevip.Config) ([]string, error) {
|
||||
var localEndpoints []string
|
||||
|
||||
for _, subset := range ep.endpoints.Subsets {
|
||||
for _, address := range subset.Addresses {
|
||||
log.Debug("processing endpoint", "label", ep.label, "ip", address.IP)
|
||||
|
||||
// 1. Compare the Nodename
|
||||
if address.NodeName != nil && id == *address.NodeName {
|
||||
log.Debug("found local endpoint", "label", ep.label, "ip", address.IP, "hostname", address.Hostname, "nodename", *address.NodeName)
|
||||
localEndpoints = append(localEndpoints, address.IP)
|
||||
continue
|
||||
}
|
||||
// 2. Compare the Hostname (only useful if address.NodeName is not available)
|
||||
if id == address.Hostname {
|
||||
log.Debug("found local endpoint", "label", ep.label, "ip", address.IP, "hostname", address.Hostname)
|
||||
localEndpoints = append(localEndpoints, address.IP)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return localEndpoints, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) UpdateServiceAnnotation(endpoint string, _ string, service *v1.Service, clientSet *kubernetes.Clientset) error {
|
||||
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
// Retrieve the latest version of Deployment before attempting update
|
||||
// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
|
||||
currentService, err := clientSet.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentServiceCopy := currentService.DeepCopy()
|
||||
if currentServiceCopy.Annotations == nil {
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
currentServiceCopy.Annotations[kubevip.ActiveEndpoint] = endpoint
|
||||
|
||||
_, err = clientSet.CoreV1().Services(currentService.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Error("error updating Service Spec", "label", ep.GetLabel(), "name", currentServiceCopy.Name, "err", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if retryErr != nil {
|
||||
log.Error("failed to set Services", "label", ep.GetLabel(), "err", retryErr)
|
||||
return retryErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetLabel() string {
|
||||
return ep.label
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetProtocol() string {
|
||||
return ""
|
||||
}
|
||||
139
pkg/endpoints/providers/endpointslices.go
Normal file
139
pkg/endpoints/providers/endpointslices.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
"k8s.io/client-go/util/retry"
|
||||
)
|
||||
|
||||
type Endpointslices struct {
|
||||
label string
|
||||
endpoints *discoveryv1.EndpointSlice
|
||||
}
|
||||
|
||||
func NewEndpointslices() Provider {
|
||||
return &Endpointslices{
|
||||
label: "endpointslices",
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *Endpointslices) CreateRetryWatcher(ctx context.Context, clientSet *kubernetes.Clientset,
|
||||
service *v1.Service) (*watchtools.RetryWatcher, error) {
|
||||
labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{"kubernetes.io/service-name": service.Name}}
|
||||
|
||||
opts := metav1.ListOptions{
|
||||
LabelSelector: labels.Set(labelSelector.MatchLabels).String(),
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctx, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return clientSet.DiscoveryV1().EndpointSlices(service.Namespace).Watch(ctx, opts)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] error creating endpointslices watcher: %s", ep.label, err.Error())
|
||||
}
|
||||
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
func (ep *Endpointslices) LoadObject(endpoints runtime.Object, cancel context.CancelFunc) error {
|
||||
eps, ok := endpoints.(*discoveryv1.EndpointSlice)
|
||||
if !ok {
|
||||
cancel()
|
||||
return fmt.Errorf("[%s] error casting endpoints to v1.Endpoints struct", ep.label)
|
||||
}
|
||||
ep.endpoints = eps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpointslices) GetAllEndpoints() ([]string, error) {
|
||||
result := []string{}
|
||||
for _, ep := range ep.endpoints.Endpoints {
|
||||
result = append(result, ep.Addresses...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ep *Endpointslices) GetLocalEndpoints(id string, _ *kubevip.Config) ([]string, error) {
|
||||
var localEndpoints []string
|
||||
for _, endpoint := range ep.endpoints.Endpoints {
|
||||
if endpoint.Conditions.Serving == nil || !*endpoint.Conditions.Serving {
|
||||
continue
|
||||
}
|
||||
for _, address := range endpoint.Addresses {
|
||||
log.Debug("processing endpoint", "provider", ep.label, "ip", address)
|
||||
|
||||
// 1. Compare the Nodename
|
||||
if endpoint.NodeName != nil && id == *endpoint.NodeName {
|
||||
if endpoint.Hostname != nil {
|
||||
log.Debug("found endpoint", "provider", ep.label, "ip", address, "hostname", *endpoint.Hostname, "nodename", *endpoint.NodeName)
|
||||
} else {
|
||||
log.Debug("found endpoint", "provider", ep.label, "ip", address, "nodename", *endpoint.NodeName)
|
||||
}
|
||||
localEndpoints = append(localEndpoints, address)
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Compare the Hostname (only useful if endpoint.NodeName is not available)
|
||||
if endpoint.Hostname != nil && id == *endpoint.Hostname {
|
||||
log.Debug("found endpoint", "provider", ep.label, "ip", address, "hostname", *endpoint.Hostname)
|
||||
localEndpoints = append(localEndpoints, address)
|
||||
}
|
||||
}
|
||||
}
|
||||
return localEndpoints, nil
|
||||
}
|
||||
|
||||
func (ep *Endpointslices) UpdateServiceAnnotation(endpoint, endpointIPv6 string, service *v1.Service, clientSet *kubernetes.Clientset) error {
|
||||
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
// Retrieve the latest version of Deployment before attempting update
|
||||
// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
|
||||
currentService, err := clientSet.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentServiceCopy := currentService.DeepCopy()
|
||||
if currentServiceCopy.Annotations == nil {
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
currentServiceCopy.Annotations[kubevip.ActiveEndpoint] = endpoint
|
||||
currentServiceCopy.Annotations[kubevip.ActiveEndpointIPv6] = endpointIPv6
|
||||
|
||||
_, err = clientSet.CoreV1().Services(currentService.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Error("error updating Service Spec", "provider", ep.label, "service name", currentServiceCopy.Name, "err", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if retryErr != nil {
|
||||
log.Error("failed to set Services", "provider", ep.label, "err", retryErr)
|
||||
return retryErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpointslices) GetLabel() string {
|
||||
return ep.label
|
||||
}
|
||||
|
||||
func (ep *Endpointslices) GetProtocol() string {
|
||||
return string(ep.endpoints.AddressType)
|
||||
}
|
||||
22
pkg/endpoints/providers/interface.go
Normal file
22
pkg/endpoints/providers/interface.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
CreateRetryWatcher(context.Context, *kubernetes.Clientset,
|
||||
*v1.Service) (*watchtools.RetryWatcher, error)
|
||||
GetAllEndpoints() ([]string, error)
|
||||
GetLocalEndpoints(string, *kubevip.Config) ([]string, error)
|
||||
GetLabel() string
|
||||
UpdateServiceAnnotation(string, string, *v1.Service, *kubernetes.Clientset) error
|
||||
LoadObject(runtime.Object, context.CancelFunc) error
|
||||
GetProtocol() string
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package equinixmetal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/packethost/packngo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// BGPLookup will use the Equinix Metal API functions to populate the BGP information
|
||||
func BGPLookup(c *packngo.Client, k *kubevip.Config) error {
|
||||
var thisDevice *packngo.Device
|
||||
if k.MetalProjectID == "" {
|
||||
proj := findProject(k.MetalProject, c)
|
||||
if proj == nil {
|
||||
return fmt.Errorf("Unable to find Project [%s]", k.MetalProject)
|
||||
}
|
||||
thisDevice = findSelf(c, proj.ID)
|
||||
} else {
|
||||
thisDevice = findSelf(c, k.MetalProjectID)
|
||||
}
|
||||
if thisDevice == nil {
|
||||
return fmt.Errorf("Unable to find local/this device in Equinix Metal API")
|
||||
}
|
||||
|
||||
log.Infof("Querying BGP settings for [%s]", thisDevice.Hostname)
|
||||
neighbours, _, err := c.Devices.ListBGPNeighbors(thisDevice.ID, &packngo.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Ensure neighbours exist (and it's enabled)
|
||||
if len(neighbours) == 0 {
|
||||
return fmt.Errorf("The server [%s]/[%s] has no BGP neighbours, ensure BGP is enabled", thisDevice.Hostname, thisDevice.ID)
|
||||
}
|
||||
|
||||
// Add a warning (TODO)
|
||||
if len(neighbours) > 1 {
|
||||
log.Warnf("There are [%d] neighbours, only designed to manage one", len(neighbours))
|
||||
}
|
||||
|
||||
// Ensure a peer exists
|
||||
if len(neighbours[0].PeerIps) == 0 {
|
||||
return fmt.Errorf("The server [%s]/[%s] has no BGP peers, ensure BGP is enabled", thisDevice.Hostname, thisDevice.ID)
|
||||
}
|
||||
|
||||
k.BGPConfig.RouterID = neighbours[0].CustomerIP
|
||||
k.BGPConfig.AS = uint32(neighbours[0].CustomerAs)
|
||||
|
||||
// Add the peer(s)
|
||||
for x := range neighbours[0].PeerIps {
|
||||
peer := bgp.Peer{
|
||||
Address: neighbours[0].PeerIps[x],
|
||||
AS: uint32(neighbours[0].PeerAs),
|
||||
MultiHop: neighbours[0].Multihop,
|
||||
Password: neighbours[0].Md5Password,
|
||||
}
|
||||
k.BGPConfig.Peers = append(k.BGPConfig.Peers, peer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package equinixmetal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/packethost/packngo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// AttachEIP will use the Equinix Metal APIs to move an EIP and attach to a host
|
||||
func AttachEIP(c *packngo.Client, k *kubevip.Config, _ string) error {
|
||||
// Use MetalProjectID if it is defined
|
||||
projID := k.MetalProjectID
|
||||
|
||||
if projID == "" {
|
||||
// Fallback to attempting to find the project by name
|
||||
proj := findProject(k.MetalProject, c)
|
||||
if proj == nil {
|
||||
return fmt.Errorf("unable to find Project [%s]", k.MetalProject)
|
||||
}
|
||||
|
||||
projID = proj.ID
|
||||
}
|
||||
|
||||
// Prefer Address over VIP
|
||||
vip := k.Address
|
||||
if vip == "" {
|
||||
vip = k.VIP
|
||||
}
|
||||
|
||||
ips, _, _ := c.ProjectIPs.List(projID, &packngo.ListOptions{})
|
||||
for _, ip := range ips {
|
||||
// Find the device id for our EIP
|
||||
if ip.Address == vip {
|
||||
log.Infof("Found EIP ->%s ID -> %s\n", ip.Address, ip.ID)
|
||||
// If attachments already exist then remove them
|
||||
if len(ip.Assignments) != 0 {
|
||||
hrefID := path.Base(ip.Assignments[0].Href)
|
||||
_, err := c.DeviceIPs.Unassign(hrefID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to unassign deviceIP %q: %v", hrefID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup this server through the Equinix Metal API
|
||||
thisDevice := findSelf(c, projID)
|
||||
if thisDevice == nil {
|
||||
return fmt.Errorf("unable to find local/this device in Equinix Metal API")
|
||||
}
|
||||
|
||||
// Assign the EIP to this device
|
||||
log.Infof("Assigning EIP to -> %s\n", thisDevice.Hostname)
|
||||
_, _, err := c.DeviceIPs.Assign(thisDevice.ID, &packngo.AddressStruct{
|
||||
Address: vip,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package equinixmetal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/packethost/packngo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func findProject(project string, c *packngo.Client) *packngo.Project {
|
||||
l := &packngo.ListOptions{Includes: []string{project}}
|
||||
ps, _, err := c.Projects.List(l)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
for _, p := range ps {
|
||||
|
||||
// Find our project
|
||||
if p.Name == project {
|
||||
return &p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findSelf(c *packngo.Client, projectID string) *packngo.Device {
|
||||
// Go through devices
|
||||
dev, _, _ := c.Devices.List(projectID, &packngo.ListOptions{})
|
||||
for _, d := range dev {
|
||||
me, _ := os.Hostname()
|
||||
if me == d.Hostname {
|
||||
return &d
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPacketConfig will lookup the configuration from a file path
|
||||
func GetPacketConfig(providerConfig string) (string, string, error) {
|
||||
var config struct {
|
||||
AuthToken string `json:"apiKey"`
|
||||
ProjectID string `json:"projectId"`
|
||||
}
|
||||
// get our token and project
|
||||
if providerConfig != "" {
|
||||
configBytes, err := os.ReadFile(providerConfig)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get read configuration file at path %s: %v", providerConfig, err)
|
||||
}
|
||||
err = json.Unmarshal(configBytes, &config)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to process json of configuration file at path %s: %v", providerConfig, err)
|
||||
}
|
||||
}
|
||||
return config.AuthToken, config.ProjectID, nil
|
||||
}
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
"hash/fnv"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
pb "go.etcd.io/etcd/api/v3/etcdserverpb"
|
||||
clientv3 "go.etcd.io/etcd/client/v3"
|
||||
"go.etcd.io/etcd/client/v3/concurrency"
|
||||
@@ -31,7 +32,7 @@ type LeaderElectionConfig struct {
|
||||
|
||||
// MemberUniqueID is the int equivalent to MemberID that allows to override the default conversion
|
||||
// from string to int using hashing.
|
||||
MemberUniqueID *int64
|
||||
MemberUniqueID *uint64
|
||||
|
||||
// LeaseDurationSeconds is the duration that non-leader candidates will
|
||||
// wait to force acquire leadership.
|
||||
@@ -73,7 +74,7 @@ func RunElectionOrDie(ctx context.Context, config *LeaderElectionConfig) {
|
||||
// RunElection blocks until leader election loop is
|
||||
// stopped by ctx or it has stopped holding the leader lease.
|
||||
func RunElection(ctx context.Context, config *LeaderElectionConfig) error {
|
||||
var memberID int64
|
||||
var memberID uint64
|
||||
if config.MemberUniqueID != nil {
|
||||
memberID = *config.MemberUniqueID
|
||||
} else {
|
||||
@@ -81,11 +82,11 @@ func RunElection(ctx context.Context, config *LeaderElectionConfig) error {
|
||||
if _, err := h.Write(append([]byte(config.Name), []byte(config.MemberID)...)); err != nil {
|
||||
return err
|
||||
}
|
||||
memberID = int64(h.Sum64())
|
||||
memberID = h.Sum64()
|
||||
}
|
||||
|
||||
ttl := config.LeaseDurationSeconds
|
||||
r := &pb.LeaseGrantRequest{TTL: ttl, ID: memberID}
|
||||
r := &pb.LeaseGrantRequest{TTL: ttl, ID: int64(memberID)} //nolint
|
||||
lease, err := clientv3.RetryLeaseClient(
|
||||
config.EtcdConfig.Client,
|
||||
).LeaseGrant(ctx, r)
|
||||
@@ -147,9 +148,9 @@ watcher:
|
||||
|
||||
m.isLeader = true
|
||||
m.key = m.election.Key() // by this time, this should already be set, since Campaign has already returned
|
||||
log.Debugf("[%s] Marking self as leader with key %s\n", m.memberID, m.key)
|
||||
log.Debug("Marking self as leader with key", "id", m.memberID, "key", m.key)
|
||||
case response := <-changes:
|
||||
log.Debugf("[%s] Leader Changes: %+v\n", m.memberID, response)
|
||||
log.Debug("Leader Changes", "id", m.memberID, "response", response)
|
||||
if len(response.Kvs) == 0 {
|
||||
// There is a race condition where just after we stop being the leader
|
||||
// if there are no more leaders, we might get a response with no key-values
|
||||
@@ -181,15 +182,15 @@ watcher:
|
||||
m.callbacks.OnStoppedLeading()
|
||||
}
|
||||
|
||||
log.Debugf("[%s] Exiting watcher\n", m.memberID)
|
||||
log.Debug("Exiting watcher", "id", m.memberID)
|
||||
}
|
||||
|
||||
func (m *member) tryToBeLeader(ctx context.Context) {
|
||||
if err := m.election.Campaign(ctx, m.memberID); err != nil {
|
||||
log.Errorf("Failed trying to become the leader: %s", err)
|
||||
log.Error("Failed trying to become the leader", "err", err)
|
||||
// Resign just in case we acquired leadership just before failing
|
||||
if err := m.election.Resign(m.client.Ctx()); err != nil {
|
||||
log.Warnf("Failed to resign after we failed becoming the leader, this might not be a problem if we were never the leader: %s", err)
|
||||
log.Warn("Failed to resign after we failed becoming the leader, this might not be a problem if we were never the leader", "err", err)
|
||||
}
|
||||
return
|
||||
// TODO: what to do here?
|
||||
@@ -209,7 +210,7 @@ func (m *member) tryToBeLeader(ctx context.Context) {
|
||||
// the previous leader to detect the new leadership (if there was one) and
|
||||
// stop its processes
|
||||
// TODO: is this too cautious?
|
||||
log.Debugf("[%s] Waiting %d seconds before running OnStartedLeading", m.memberID, m.leaseTTL)
|
||||
log.Debug("timeout before OnStartedLeading", "id", m.memberID, "timeout", m.leaseTTL)
|
||||
time.Sleep(time.Second * time.Duration(m.leaseTTL))
|
||||
|
||||
// We are the leader, execute our code
|
||||
@@ -220,7 +221,7 @@ func (m *member) tryToBeLeader(ctx context.Context) {
|
||||
|
||||
func (m *member) resignOnCancel(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
if err := m.election.Resign(m.client.Ctx()); err != nil {
|
||||
log.Errorf("Failed to resign after the context was canceled: %s", err)
|
||||
if err := m.election.Resign(m.client.Ctx()); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Error("Failed to resign after the context was canceled", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestRunElectionWithMemberIDCollision(t *testing.T) {
|
||||
Client: cli,
|
||||
},
|
||||
Name: electionName,
|
||||
MemberID: "my-host",
|
||||
MemberID: randomElectionNameForTest("my-host"),
|
||||
LeaseDurationSeconds: 1,
|
||||
Callbacks: etcd.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
@@ -90,19 +90,21 @@ func TestRunElectionWithTwoMembersAndReelection(t *testing.T) {
|
||||
|
||||
config1 := configBase
|
||||
config1.EtcdConfig.Client = cliMember1
|
||||
config1.MemberID = "my-host"
|
||||
uniqueID := rand.Int63()
|
||||
config1.MemberID = randomElectionNameForTest("my-host")
|
||||
uniqueID := rand.Uint64()
|
||||
config1.MemberUniqueID = &uniqueID
|
||||
config1.Callbacks = baseCallbacksForName(config1.MemberID)
|
||||
syncMembers := make(chan (any))
|
||||
config1.Callbacks.OnStartedLeading = func(_ context.Context) {
|
||||
log.Println("I'm my-host, the new leader!!!!")
|
||||
log.Println("Loosing the leadership on purpose by stopping renewing the lease")
|
||||
close(syncMembers)
|
||||
log.Println("Losing the leadership on purpose by stopping renewing the lease")
|
||||
g.Expect(cliMember1.Lease.Close()).To(Succeed())
|
||||
log.Println("Member1 leases closed")
|
||||
}
|
||||
|
||||
config2 := configBase
|
||||
config2.MemberID = "my-other-host"
|
||||
config2.MemberID = randomElectionNameForTest("my-other-host")
|
||||
config2.Callbacks = baseCallbacksForName(config2.MemberID)
|
||||
config2.Callbacks.OnStartedLeading = func(_ context.Context) {
|
||||
log.Println("I'm my-other-host, the new leader!!!!")
|
||||
@@ -116,14 +118,14 @@ func TestRunElectionWithTwoMembersAndReelection(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
g.Expect(etcd.RunElection(member1Ctx, &config1)).To(Succeed())
|
||||
log.Println("Member1 routine done")
|
||||
log.Printf("%s routine done\n", config1.MemberID)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(time.Millisecond * 50) // Make sure member1 becomes leader
|
||||
<-syncMembers
|
||||
g.Expect(etcd.RunElection(member2Ctx, &config2)).To(Succeed())
|
||||
log.Println("Member2 routine done")
|
||||
log.Printf("%s routine done\n", config2.MemberID)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
516
pkg/instance/instance.go
Normal file
516
pkg/instance/instance.go
Normal file
@@ -0,0 +1,516 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"log/slog"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
||||
"github.com/kube-vip/kube-vip/pkg/sysctl"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
// Instance defines an instance of everything needed to manage vips
|
||||
type Instance struct {
|
||||
// Virtual IP / Load Balancer configuration
|
||||
VIPConfigs []*kubevip.Config
|
||||
|
||||
// cluster instances
|
||||
Clusters []*cluster.Cluster
|
||||
|
||||
// Service uses DHCP
|
||||
IsDHCP bool
|
||||
DHCPInterface string
|
||||
DHCPInterfaceHwaddr string
|
||||
DHCPInterfaceIP string
|
||||
DHCPHostname string
|
||||
DHCPClient *vip.DHCPClient
|
||||
|
||||
// External Gateway IP the service is forwarded from
|
||||
UPNPGatewayIPs []string
|
||||
|
||||
// Kubernetes service mapping
|
||||
ServiceSnapshot *v1.Service
|
||||
}
|
||||
|
||||
type Port struct {
|
||||
Port uint16
|
||||
Type string
|
||||
}
|
||||
|
||||
func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterface.Manager, arpMgr *arp.Manager) (*Instance, error) {
|
||||
instanceAddresses, _ := FetchServiceAddresses(svc)
|
||||
|
||||
var newVips []*kubevip.Config
|
||||
var link netlink.Link
|
||||
var err error
|
||||
|
||||
for _, address := range instanceAddresses {
|
||||
// Detect if we're using a specific interface for services
|
||||
var svcInterface string
|
||||
svcInterface = svc.Annotations[kubevip.ServiceInterface] // If the service has a specific interface defined, then use it
|
||||
if svcInterface == kubevip.Auto {
|
||||
link, err = autoFindInterface(address)
|
||||
if err != nil {
|
||||
log.Error("automatically discover network interface for annotated IP", "address", address, "err", err)
|
||||
} else {
|
||||
if link == nil {
|
||||
log.Error("automatically discover network interface for annotated IP address", "address", address)
|
||||
}
|
||||
}
|
||||
if link == nil {
|
||||
svcInterface = ""
|
||||
} else {
|
||||
svcInterface = getAutoInterfaceName(link, config.Interface)
|
||||
}
|
||||
}
|
||||
// If it is still blank then use the
|
||||
if svcInterface == "" {
|
||||
switch config.ServicesInterface {
|
||||
case kubevip.Auto:
|
||||
link, err = autoFindInterface(address)
|
||||
if err != nil {
|
||||
log.Error("failed to automatically discover network interface for address", "ip", address, "err", err, "interface", config.Interface)
|
||||
} else if link == nil {
|
||||
log.Error("failed to automatically discover network interface for address", "ip", address, "defaulting to", config.Interface)
|
||||
}
|
||||
svcInterface = getAutoInterfaceName(link, config.Interface)
|
||||
case "":
|
||||
svcInterface = config.Interface
|
||||
default:
|
||||
svcInterface = config.ServicesInterface
|
||||
}
|
||||
}
|
||||
|
||||
if link == nil {
|
||||
if link, err = netlink.LinkByName(svcInterface); err != nil {
|
||||
return nil, fmt.Errorf("failed to get interface %s: %w", svcInterface, err)
|
||||
}
|
||||
if link == nil {
|
||||
return nil, fmt.Errorf("failed to get interface %s", svcInterface)
|
||||
}
|
||||
}
|
||||
|
||||
cidrs := vip.Split(config.VIPSubnet)
|
||||
|
||||
ipv4AutoSubnet := false
|
||||
ipv6AutoSubnet := false
|
||||
if cidrs[0] == kubevip.Auto {
|
||||
ipv4AutoSubnet = true
|
||||
}
|
||||
|
||||
if len(cidrs) > 1 && cidrs[1] == kubevip.Auto {
|
||||
ipv6AutoSubnet = true
|
||||
}
|
||||
|
||||
if (config.Address != "" || config.VIP != "") && (ipv4AutoSubnet || ipv6AutoSubnet) {
|
||||
return nil, fmt.Errorf("auto subnet discovery cannot be used if VIP address was provided")
|
||||
}
|
||||
|
||||
subnet := ""
|
||||
var err error
|
||||
if utils.IsIPv4(address) {
|
||||
if ipv4AutoSubnet {
|
||||
subnet, err = autoFindSubnet(link, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to automatically find subnet for service %s/%s with IP address %s on interface %s: %w", svc.Namespace, svc.Name, address, svcInterface, err)
|
||||
}
|
||||
} else {
|
||||
if cidrs[0] != "" && cidrs[0] != kubevip.Auto {
|
||||
subnet = cidrs[0]
|
||||
} else {
|
||||
subnet = "32"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ipv6AutoSubnet {
|
||||
subnet, err = autoFindSubnet(link, address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to automatically find subnet for service %s/%s with IP address %s on interface %s: %w", svc.Namespace, svc.Name, address, svcInterface, err)
|
||||
}
|
||||
} else {
|
||||
if len(cidrs) > 1 && cidrs[1] != "" && cidrs[1] != kubevip.Auto {
|
||||
subnet = cidrs[1]
|
||||
} else {
|
||||
subnet = "128"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new Virtual IP configuration
|
||||
newVips = append(newVips, &kubevip.Config{
|
||||
VIP: address,
|
||||
Interface: svcInterface,
|
||||
SingleNode: true,
|
||||
EnableARP: config.EnableARP,
|
||||
EnableBGP: config.EnableBGP,
|
||||
VIPSubnet: subnet,
|
||||
EnableRoutingTable: config.EnableRoutingTable,
|
||||
RoutingTableID: config.RoutingTableID,
|
||||
RoutingTableType: config.RoutingTableType,
|
||||
RoutingProtocol: config.RoutingProtocol,
|
||||
ArpBroadcastRate: config.ArpBroadcastRate,
|
||||
EnableServiceSecurity: config.EnableServiceSecurity,
|
||||
DNSMode: config.DNSMode,
|
||||
DisableServiceUpdates: config.DisableServiceUpdates,
|
||||
EnableServicesElection: config.EnableServicesElection,
|
||||
PreserveVIPOnLeadershipLoss: config.PreserveVIPOnLeadershipLoss,
|
||||
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
|
||||
EnableLeaderElection: config.EnableLeaderElection,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create new service
|
||||
instance := &Instance{
|
||||
//UID: instanceUID,
|
||||
//VIPs: instanceAddresses,
|
||||
ServiceSnapshot: svc,
|
||||
}
|
||||
// for _, port := range svc.Spec.Ports {
|
||||
// instance.ExternalPorts = append(instance.ExternalPorts, Port{
|
||||
// Port: uint16(port.Port), //nolint
|
||||
// Type: string(port.Protocol),
|
||||
// })
|
||||
// }
|
||||
|
||||
if svc.Annotations != nil {
|
||||
instance.DHCPInterfaceHwaddr = svc.Annotations[kubevip.HwAddrKey]
|
||||
instance.DHCPInterfaceIP = svc.Annotations[kubevip.RequestedIP]
|
||||
instance.DHCPHostname = svc.Annotations[kubevip.LoadbalancerHostname]
|
||||
}
|
||||
|
||||
configPorts := make([]kubevip.Port, 0)
|
||||
for _, p := range svc.Spec.Ports {
|
||||
configPorts = append(configPorts, kubevip.Port{
|
||||
Type: string(p.Protocol),
|
||||
Port: int(p.Port),
|
||||
})
|
||||
}
|
||||
// Generate Load Balancer config
|
||||
newLB := kubevip.LoadBalancer{
|
||||
Name: fmt.Sprintf("%s-load-balancer", svc.Name),
|
||||
Ports: configPorts,
|
||||
BindToVip: true,
|
||||
}
|
||||
for _, vip := range newVips {
|
||||
// Add Load Balancer Configuration
|
||||
vip.LoadBalancers = append(vip.LoadBalancers, newLB)
|
||||
}
|
||||
// Create Add configuration to the new service
|
||||
instance.VIPConfigs = newVips
|
||||
|
||||
// If this was purposely created with the address 0.0.0.0,
|
||||
// we will create a macvlan on the main interface and a DHCP client
|
||||
// TODO: Consider how best to handle DHCP with multiple addresses
|
||||
if len(instanceAddresses) == 1 && instanceAddresses[0] == "0.0.0.0" {
|
||||
err := instance.startDHCP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case err := <-instance.DHCPClient.ErrorChannel():
|
||||
return nil, fmt.Errorf("error starting DHCP for %s/%s: error: %s",
|
||||
instance.ServiceSnapshot.Namespace, instance.ServiceSnapshot.Name, err)
|
||||
case ip := <-instance.DHCPClient.IPChannel():
|
||||
instance.VIPConfigs[0].Interface = instance.DHCPInterface
|
||||
instance.VIPConfigs[0].VIP = ip
|
||||
instance.DHCPInterfaceIP = ip
|
||||
}
|
||||
}
|
||||
|
||||
for _, vipConfig := range instance.VIPConfigs {
|
||||
c, err := cluster.InitCluster(vipConfig, false, intfMgr, arpMgr)
|
||||
if err != nil {
|
||||
log.Error("Failed to add Service %s/%s", svc.Namespace, svc.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range c.Network {
|
||||
c.Network[i].SetServicePorts(svc)
|
||||
}
|
||||
|
||||
instance.Clusters = append(instance.Clusters, c)
|
||||
log.Info("(svcs) adding VIP", "ip", vipConfig.VIP, "interface", vipConfig.Interface, "namespace", svc.Namespace, "name", svc.Name)
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func autoFindInterface(ip string) (netlink.Link, error) {
|
||||
links, err := netlink.LinkList()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list network interfaces: %w", err)
|
||||
}
|
||||
|
||||
address := net.ParseIP(ip)
|
||||
|
||||
family := netlink.FAMILY_V4
|
||||
|
||||
if address.To4() == nil {
|
||||
family = netlink.FAMILY_V6
|
||||
}
|
||||
|
||||
for _, link := range links {
|
||||
addr, err := netlink.AddrList(link, family)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get IP addresses for interface %s: %w", link.Attrs().Name, err)
|
||||
}
|
||||
for _, a := range addr {
|
||||
if a.IPNet.Contains(address) {
|
||||
return link, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func autoFindSubnet(link netlink.Link, ip string) (string, error) {
|
||||
address := net.ParseIP(ip)
|
||||
|
||||
family := netlink.FAMILY_V4
|
||||
if address.To4() == nil {
|
||||
family = netlink.FAMILY_V6
|
||||
}
|
||||
|
||||
addr, err := netlink.AddrList(link, family)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get IP addresses for interface %s: %w", link.Attrs().Name, err)
|
||||
}
|
||||
for _, a := range addr {
|
||||
if a.IPNet.Contains(address) {
|
||||
m, _ := a.IPNet.Mask.Size()
|
||||
return strconv.Itoa(m), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to find suitable subnet for address %s", ip)
|
||||
}
|
||||
|
||||
func getAutoInterfaceName(link netlink.Link, defaultInterface string) string {
|
||||
if link == nil {
|
||||
return defaultInterface
|
||||
}
|
||||
return link.Attrs().Name
|
||||
}
|
||||
|
||||
func (i *Instance) startDHCP() error {
|
||||
if len(i.VIPConfigs) != 1 {
|
||||
return fmt.Errorf("DHCP requires exactly 1 VIP config, got: %v", len(i.VIPConfigs))
|
||||
}
|
||||
parent, err := netlink.LinkByName(i.VIPConfigs[0].Interface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding VIP Interface, for building DHCP Link : %v", err)
|
||||
}
|
||||
|
||||
// Generate name from UID
|
||||
interfaceName := fmt.Sprintf("vip-%s", i.ServiceSnapshot.UID[0:8])
|
||||
|
||||
// Check if the interface doesn't exist first
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
log.Info("creating new macvlan interface for DHCP", "interface", interfaceName)
|
||||
|
||||
hwaddr, err := net.ParseMAC(i.DHCPInterfaceHwaddr)
|
||||
if i.DHCPInterfaceHwaddr != "" && err != nil {
|
||||
return err
|
||||
} else if hwaddr == nil {
|
||||
hwaddr, err = net.ParseMAC(vip.GenerateMac())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("new macvlan interface", "interface", interfaceName, "hardware address", hwaddr)
|
||||
mac := &netlink.Macvlan{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: interfaceName,
|
||||
ParentIndex: parent.Attrs().Index,
|
||||
HardwareAddr: hwaddr,
|
||||
},
|
||||
Mode: netlink.MACVLAN_MODE_DEFAULT,
|
||||
}
|
||||
|
||||
err = netlink.LinkAdd(mac)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add %s: %v", interfaceName, err)
|
||||
}
|
||||
|
||||
err = netlink.LinkSetUp(mac)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not bring up interface [%s] : %v", interfaceName, err)
|
||||
}
|
||||
|
||||
iface, err = net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding new DHCP interface by name [%v]", err)
|
||||
}
|
||||
} else {
|
||||
log.Info("Using existing macvlan interface for DHCP", "interface", interfaceName)
|
||||
}
|
||||
|
||||
// Default rp_filter setting (https://github.com/kube-vip/kube-vip/issues/1170)
|
||||
rpfilterSetting := "0"
|
||||
|
||||
// Check if we need to set an override rp_filter value for the interface
|
||||
if i.ServiceSnapshot.Annotations[kubevip.RPFilter] != "" {
|
||||
// Check the rp_filter value
|
||||
rpFilter, err := strconv.Atoi(i.ServiceSnapshot.Annotations[kubevip.RPFilter])
|
||||
if err != nil {
|
||||
slog.Error("[DHCP] unable to process rp_filter", "value", rpFilter)
|
||||
} else {
|
||||
if rpFilter >= 0 && rpFilter < 3 { // Ensure the value is 0,1,2
|
||||
rpfilterSetting = i.ServiceSnapshot.Annotations[kubevip.RPFilter]
|
||||
} else {
|
||||
slog.Error("[DHCP] rp_filter value not within range 0-2", "value", rpFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = sysctl.WriteProcSys("/proc/sys/net/ipv4/conf/"+interfaceName+"/rp_filter", rpfilterSetting)
|
||||
if err != nil {
|
||||
slog.Error("[DHCP] unable to write rp_filter", "value", rpfilterSetting, "err", err)
|
||||
}
|
||||
var initRebootFlag bool
|
||||
if i.DHCPInterfaceIP != "" {
|
||||
initRebootFlag = true
|
||||
}
|
||||
|
||||
client := vip.NewDHCPClient(iface, initRebootFlag, i.DHCPInterfaceIP)
|
||||
|
||||
// Add hostname to dhcp client if annotated
|
||||
if i.DHCPHostname != "" {
|
||||
log.Info("Hostname specified for dhcp lease", "interface", interfaceName, "hostname", i.DHCPHostname)
|
||||
client.WithHostName(i.DHCPHostname)
|
||||
}
|
||||
|
||||
go client.Start()
|
||||
|
||||
// Set that DHCP is enabled
|
||||
i.IsDHCP = true
|
||||
// Set the name of the interface so that it can be removed on Service deletion
|
||||
i.DHCPInterface = interfaceName
|
||||
i.DHCPInterfaceHwaddr = iface.HardwareAddr.String()
|
||||
// Add the client so that we can call it to stop function
|
||||
i.DHCPClient = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchLoadBalancerIngressAddresses tries to get the addresses from status.loadBalancerIP
|
||||
func FetchLoadBalancerIngress(s *v1.Service) ([]string, []string) {
|
||||
// If the service has no status, return empty
|
||||
lbStatusAddresses := []string{}
|
||||
lbStatusHostnames := []string{}
|
||||
if len(s.Status.LoadBalancer.Ingress) == 0 {
|
||||
return lbStatusAddresses, lbStatusHostnames
|
||||
}
|
||||
|
||||
for _, ingress := range s.Status.LoadBalancer.Ingress {
|
||||
if ingress.IP != "" {
|
||||
lbStatusAddresses = append(lbStatusAddresses, ingress.IP)
|
||||
}
|
||||
if ingress.Hostname != "" {
|
||||
lbStatusHostnames = append(lbStatusHostnames, ingress.Hostname)
|
||||
}
|
||||
}
|
||||
return lbStatusAddresses, lbStatusHostnames
|
||||
}
|
||||
|
||||
// FetchServiceAddresses tries to get the addresses from annotations
|
||||
// kube-vip.io/loadbalancerIPs, then from spec.loadbalancerIP
|
||||
func FetchServiceAddresses(s *v1.Service) ([]string, []string) {
|
||||
annotationAvailable := false
|
||||
if s.Annotations != nil {
|
||||
|
||||
if v, annotationAvailable := s.Annotations[kubevip.LoadbalancerIPAnnotation]; annotationAvailable {
|
||||
ips := strings.Split(v, ",")
|
||||
var trimmedIPs []string
|
||||
var trimmedHostnames []string
|
||||
for _, a := range ips {
|
||||
a = strings.TrimSpace(a)
|
||||
ip := net.ParseIP(a)
|
||||
if ip == nil {
|
||||
// this is probably a DNS name
|
||||
trimmedHostnames = append(trimmedHostnames, a)
|
||||
} else {
|
||||
trimmedIPs = append(trimmedIPs, ip.String())
|
||||
}
|
||||
}
|
||||
return trimmedIPs, trimmedHostnames
|
||||
}
|
||||
}
|
||||
|
||||
lbStatusAddresses := []string{}
|
||||
lbStatusHostnames := []string{}
|
||||
if !annotationAvailable {
|
||||
lbStatusAddresses, lbStatusHostnames = FetchLoadBalancerIngress(s)
|
||||
}
|
||||
|
||||
// Spec.LoadBalancerIP legacy handling
|
||||
// if the loadBalancerIP is different from Status.LoadBalancer.Ingress IPs
|
||||
// return the legacy LB as spec wins over status.
|
||||
if lbIP := net.ParseIP(s.Spec.LoadBalancerIP); lbIP != nil && len(lbStatusAddresses) > 0 {
|
||||
isLbIPv4 := utils.IsIPv4(s.Spec.LoadBalancerIP)
|
||||
for _, a := range lbStatusAddresses {
|
||||
if lbStatusIP := net.ParseIP(a); lbStatusIP != nil && utils.IsIPv4(a) == isLbIPv4 && !lbIP.Equal(lbStatusIP) {
|
||||
return []string{s.Spec.LoadBalancerIP}, []string{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(lbStatusAddresses) > 0 || len(lbStatusHostnames) > 0 {
|
||||
return lbStatusAddresses, lbStatusHostnames
|
||||
}
|
||||
|
||||
if s.Spec.LoadBalancerIP != "" {
|
||||
return []string{s.Spec.LoadBalancerIP}, []string{}
|
||||
}
|
||||
|
||||
return []string{}, []string{}
|
||||
}
|
||||
|
||||
func FindServiceInstance(svc *v1.Service, instances []*Instance) *Instance {
|
||||
log.Debug("finding service", "namespace", svc.Namespace, "name", svc.Name, "UID", svc.UID)
|
||||
for i := range instances {
|
||||
log.Debug("saved service", "instance", i, "UID", instances[i].ServiceSnapshot.UID)
|
||||
if instances[i].ServiceSnapshot.UID == svc.UID {
|
||||
return instances[i]
|
||||
}
|
||||
}
|
||||
log.Debug("instance not found", "namespace", svc.Namespace, "name", svc.Name, "UID", svc.UID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindServiceInstanceWithTimeout(svc *v1.Service, instances []*Instance) *Instance {
|
||||
log.Debug("finding service with timeout", "namespace", svc.Namespace, "name", svc.Name, "UID", svc.UID)
|
||||
ticker := time.NewTicker(time.Millisecond * 200)
|
||||
defer ticker.Stop()
|
||||
to := time.NewTimer(time.Second * 60)
|
||||
defer to.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-to.C:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
for i := range instances {
|
||||
log.Debug("saved service", "instance", i, "UID", instances[i].ServiceSnapshot.UID)
|
||||
if instances[i].ServiceSnapshot.UID == svc.UID {
|
||||
return instances[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,12 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
TableFilter = "filter"
|
||||
ChainInput = "INPUT"
|
||||
TableFilter = "filter"
|
||||
TableMangle = "mangle"
|
||||
TableNat = "nat"
|
||||
ChainInput = "INPUT"
|
||||
ChainPREROUTING = "PREROUTING"
|
||||
ChainPOSTROUTING = "POSTROUTING"
|
||||
)
|
||||
|
||||
type IPTables struct {
|
||||
|
||||
91
pkg/iptables/version.go
Normal file
91
pkg/iptables/version.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package iptables
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
BackendMode string
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
|
||||
}
|
||||
|
||||
func (v Version) Compare(other Version) int {
|
||||
if v.Major != other.Major {
|
||||
return v.Major - other.Major
|
||||
}
|
||||
if v.Minor != other.Minor {
|
||||
return v.Minor - other.Minor
|
||||
}
|
||||
return v.Patch - other.Patch
|
||||
}
|
||||
|
||||
func ParseVersion(versionString string) (Version, error) {
|
||||
re := regexp.MustCompile(`v([0-9]+)\.([0-9]+)\.([0-9]+)`)
|
||||
match := re.FindStringSubmatch(versionString)
|
||||
if len(match) != 4 {
|
||||
return Version{}, fmt.Errorf("invalid version string: %s", versionString)
|
||||
}
|
||||
major, _ := strconv.Atoi(match[1])
|
||||
minor, _ := strconv.Atoi(match[2])
|
||||
patch, _ := strconv.Atoi(match[3])
|
||||
return Version{Major: major, Minor: minor, Patch: patch}, nil
|
||||
}
|
||||
|
||||
func GetVersion() (Version, error) {
|
||||
ver := Version{}
|
||||
cmd := exec.Command("iptables", "--version")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return ver, fmt.Errorf("run cmd 'iptables --version' wtith error: %v", err)
|
||||
}
|
||||
|
||||
ver, err = ParseVersion(string(out))
|
||||
if err != nil {
|
||||
return ver, err
|
||||
}
|
||||
|
||||
nft4 := getOutput("iptables-nft-save")
|
||||
legacy4 := getOutput("iptables-legacy-save")
|
||||
|
||||
nft6 := getOutput("ip6tables-nft-save")
|
||||
legacy6 := getOutput("ip6tables-legacy-save")
|
||||
|
||||
if strings.Contains(nft4, "KUBE-IPTABLES") ||
|
||||
strings.Contains(nft6, "KUBE-IPTABLES") ||
|
||||
strings.Contains(nft4, "KUBE-KUBELET") ||
|
||||
strings.Contains(nft6, "KUBE-KUBELET") {
|
||||
ver.BackendMode = "nft"
|
||||
} else if strings.Contains(legacy4, "KUBE-IPTABLES") ||
|
||||
strings.Contains(legacy6, "KUBE-IPTABLES") ||
|
||||
strings.Contains(legacy4, "KUBE-KUBELET") ||
|
||||
strings.Contains(legacy6, "KUBE-KUBELET") {
|
||||
ver.BackendMode = "legacy"
|
||||
} else {
|
||||
nftCount := strings.Count(nft4, "\n") + strings.Count(nft6, "\n")
|
||||
legacyCount := strings.Count(legacy4, "\n") + strings.Count(legacy6, "\n")
|
||||
|
||||
if nftCount >= legacyCount {
|
||||
ver.BackendMode = "nft"
|
||||
} else {
|
||||
ver.BackendMode = "legacy"
|
||||
}
|
||||
}
|
||||
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
func getOutput(name string) string {
|
||||
cmd := exec.Command(name)
|
||||
out, _ := cmd.Output()
|
||||
return string(out)
|
||||
}
|
||||
@@ -6,31 +6,19 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "log/slog"
|
||||
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// NewClientset takes an optional configPath and creates a new clientset.
|
||||
// If the configPath is not specified, and inCluster is true, then an
|
||||
// InClusterConfig is used.
|
||||
// Also takes a hostname which allow for overriding the config's hostname
|
||||
// before generating a client.
|
||||
func NewClientset(configPath string, inCluster bool, hostname string) (*kubernetes.Clientset, error) {
|
||||
return newClientset(configPath, inCluster, hostname, time.Second*10)
|
||||
}
|
||||
|
||||
func newClientset(configPath string, inCluster bool, hostname string, timeout time.Duration) (*kubernetes.Clientset, error) {
|
||||
config, err := restConfig(configPath, inCluster, timeout)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
if len(hostname) > 0 {
|
||||
config.Host = hostname
|
||||
}
|
||||
const (
|
||||
defaultTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// NewClientset takes REST config and returns k8s clientest.
|
||||
func NewClientset(config *rest.Config) (*kubernetes.Clientset, error) {
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating kubernetes client: %s", err.Error())
|
||||
@@ -38,18 +26,36 @@ func newClientset(configPath string, inCluster bool, hostname string, timeout ti
|
||||
return clientset, nil
|
||||
}
|
||||
|
||||
// NewRestConfig takes an optional configPath and creates a new REST config for clientset.
|
||||
// If the configPath is not specified, and inCluster is true, then an
|
||||
// InClusterConfig is used.
|
||||
// Also takes a hostname which allow for overriding the config's hostname.
|
||||
func NewRestConfig(configPath string, inCluster bool, hostname string) (*rest.Config, error) {
|
||||
config, err := restConfig(configPath, inCluster, defaultTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create rest config: %w", err)
|
||||
}
|
||||
|
||||
if len(hostname) > 0 {
|
||||
config.Host = hostname
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func restConfig(kubeconfig string, inCluster bool, timeout time.Duration) (*rest.Config, error) {
|
||||
cfg, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
log.Debugf("[k8s client] we try the incluster first, this error [%v] can safely be ignored", err)
|
||||
}
|
||||
|
||||
if kubeconfig != "" && !inCluster {
|
||||
cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var cfg *rest.Config
|
||||
var err error
|
||||
if inCluster {
|
||||
if cfg, err = rest.InClusterConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to get incluster config: %w", err)
|
||||
}
|
||||
} else if kubeconfig != "" {
|
||||
if cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to build config from file '%s': %w", kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to build config from file: path to KubeConfig not specified")
|
||||
}
|
||||
|
||||
// Override some of the defaults allowing a little bit more flexibility speaking with the API server
|
||||
@@ -87,23 +93,30 @@ func findAddressFromRemoteCert(address string) ([]net.IP, error) {
|
||||
return certs[0].IPAddresses, nil
|
||||
}
|
||||
|
||||
func FindWorkingKubernetesAddress(configPath string, inCluster bool) (*kubernetes.Clientset, error) {
|
||||
func FindWorkingKubernetesAddress(configPath string, inCluster bool) (*rest.Config, error) {
|
||||
// check with loopback, and retrieve its certificate
|
||||
ips, err := findAddressFromRemoteCert("127.0.0.1:6443")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for x := range ips {
|
||||
log.Debugf("[k8s client] checking with IP address [%s]", ips[x].String())
|
||||
|
||||
k, err := newClientset(configPath, inCluster, ips[x].String()+":6443", time.Second*2)
|
||||
log.Debug("[k8s client] testing", "address", ips[x].String())
|
||||
c, err := NewRestConfig(configPath, inCluster, net.JoinHostPort(ips[x].String(), "6443"))
|
||||
if err != nil {
|
||||
log.Info(err)
|
||||
log.Error("failed to create k8s REST config", "err", err)
|
||||
}
|
||||
|
||||
c.Timeout = 2 * time.Second
|
||||
k, err := NewClientset(c)
|
||||
if err != nil {
|
||||
log.Error("failed to create k8s clientset", "err", err)
|
||||
}
|
||||
|
||||
_, err = k.DiscoveryClient.ServerVersion()
|
||||
if err == nil {
|
||||
log.Infof("[k8s client] working with IP address [%s]", ips[x].String())
|
||||
return NewClientset(configPath, inCluster, ips[x].String()+":6443")
|
||||
log.Info("[k8s client] working", "address", ips[x].String())
|
||||
c.Timeout = defaultTimeout
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unable to find a working address for the local API server [%v]", err)
|
||||
|
||||
63
pkg/kubevip/annotations.go
Normal file
63
pkg/kubevip/annotations.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package kubevip
|
||||
|
||||
const (
|
||||
// Hardware address of the host that has the VIP
|
||||
HwAddrKey = "kube-vip.io/hwaddr"
|
||||
|
||||
// The IP address that is requested
|
||||
RequestedIP = "kube-vip.io/requestedIP"
|
||||
|
||||
// The host that has the VIP
|
||||
VipHost = "kube-vip.io/vipHost"
|
||||
|
||||
// Enable Egress on a service
|
||||
Egress = "kube-vip.io/egress"
|
||||
|
||||
// Enable internal Egress
|
||||
EgressInternal = "kube-vip.io/egress-internal"
|
||||
|
||||
// Egress should be IPv6
|
||||
EgressIPv6 = "kube-vip.io/egress-ipv6"
|
||||
|
||||
// Ports that traffic is allowed to access from the egress VIP
|
||||
EgressDestinationPorts = "kube-vip.io/egress-destination-ports"
|
||||
|
||||
// Allowed incoming ports to the VIP
|
||||
EgressSourcePorts = "kube-vip.io/egress-source-ports"
|
||||
|
||||
// Allowed networks for the Egress to be enabled for
|
||||
EgressAllowedNetworks = "kube-vip.io/egress-allowed-networks"
|
||||
|
||||
// Networks that we wont Egress for
|
||||
EgressDeniedNetworks = "kube-vip.io/egress-denied-networks"
|
||||
|
||||
// The current active endpoint(pod) for the Egress VIP
|
||||
ActiveEndpoint = "kube-vip.io/active-endpoint"
|
||||
|
||||
// The current active endpoint(pod) for the Egress VIP (v6)
|
||||
ActiveEndpointIPv6 = "kube-vip.io/active-endpoint-ipv6"
|
||||
|
||||
// Flush the conntrack rules (remove existing sessions) once Egress is configured
|
||||
FlushContrack = "kube-vip.io/flush-conntrack"
|
||||
|
||||
// Configure LoadBalancer IPs instead of relying on a controller
|
||||
LoadbalancerIPAnnotation = "kube-vip.io/loadbalancerIPs"
|
||||
|
||||
// Ignore the LoadBalancer Service
|
||||
LoadbalancerIgnore = "kube-vip.io/ignore"
|
||||
|
||||
// Used to configure DHCP with a Hostname
|
||||
LoadbalancerHostname = "kube-vip.io/loadbalancerHostname"
|
||||
|
||||
// Define an interface name to bind the address of the LoadBalancer to
|
||||
ServiceInterface = "kube-vip.io/serviceInterface"
|
||||
|
||||
ServiceSecurityIgnore = "kube-vip.io/ignore-service-security"
|
||||
|
||||
// Enable UPNP on a Service
|
||||
UpnpEnabled = "kube-vip.io/forwardUPNP"
|
||||
|
||||
RPFilter = "kube-vip.io/rp_filter" // Set the return path filter for a specific service interface
|
||||
|
||||
ServiceLease = "kube-vip.io/leaseName"
|
||||
)
|
||||
234
pkg/kubevip/config_bgp.go
Normal file
234
pkg/kubevip/config_bgp.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package kubevip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
api "github.com/osrg/gobgp/v3/api"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// Peer defines a BGP Peer
|
||||
type BGPPeer struct {
|
||||
Address string
|
||||
Port uint16
|
||||
AS uint32
|
||||
Password string
|
||||
MultiHop bool
|
||||
MpbgpNexthop string
|
||||
MpbgpIPv4 string
|
||||
MpbgpIPv6 string
|
||||
}
|
||||
|
||||
// Config defines the BGP server configuration
|
||||
type BGPConfig struct {
|
||||
AS uint32
|
||||
RouterID string
|
||||
SourceIP string
|
||||
SourceIF string
|
||||
MpbgpNexthop string
|
||||
MpbgpIPv4 string
|
||||
MpbgpIPv6 string
|
||||
|
||||
HoldTime uint64
|
||||
KeepaliveInterval uint64
|
||||
|
||||
Peers []BGPPeer
|
||||
|
||||
Zebra ZebraConfig
|
||||
}
|
||||
|
||||
// Defines Zebra connection configuration. More on the topic - https://github.com/osrg/gobgp/blob/master/docs/sources/zebra.md#configuration
|
||||
type ZebraConfig struct {
|
||||
Enabled bool
|
||||
URL string
|
||||
Version uint32
|
||||
SoftwareName string
|
||||
}
|
||||
|
||||
// ParseBGPPeerConfig - take a string and parses it into an array of peers
|
||||
func ParseBGPPeerConfig(config string) (bgpPeers []BGPPeer, err error) {
|
||||
peers := strings.Split(config, ",")
|
||||
if len(peers) == 0 {
|
||||
return nil, fmt.Errorf("no BGP Peer configurations found")
|
||||
}
|
||||
|
||||
for x := range peers {
|
||||
peerStr := peers[x]
|
||||
config := strings.Split(peerStr, "/")
|
||||
peerStr = config[0]
|
||||
if peerStr == "" {
|
||||
continue
|
||||
}
|
||||
isV6Peer := peerStr[0] == '['
|
||||
|
||||
address := ""
|
||||
if isV6Peer {
|
||||
addressEndPos := strings.IndexByte(peerStr, ']')
|
||||
if addressEndPos == -1 {
|
||||
return nil, fmt.Errorf("no matching ] found for IPv6 BGP Peer")
|
||||
}
|
||||
address = peerStr[1:addressEndPos]
|
||||
peerStr = peerStr[addressEndPos+1:]
|
||||
}
|
||||
|
||||
peer := strings.Split(peerStr, ":")
|
||||
if len(peer) < 2 {
|
||||
return nil, fmt.Errorf("mandatory peering params <host>:<AS> incomplete")
|
||||
}
|
||||
|
||||
if !isV6Peer {
|
||||
address = peer[0]
|
||||
}
|
||||
|
||||
ASNumber, err := strconv.ParseUint(peer[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP Peer AS format error [%s]", peer[1])
|
||||
}
|
||||
|
||||
password := ""
|
||||
if len(peer) >= 3 {
|
||||
password = peer[2]
|
||||
}
|
||||
|
||||
multiHop := false
|
||||
if len(peer) >= 4 {
|
||||
multiHop, err = strconv.ParseBool(peer[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP MultiHop format error (true/false) [%s]", peer[1])
|
||||
}
|
||||
}
|
||||
|
||||
var port uint64
|
||||
if len(peer) >= 5 {
|
||||
port, err = strconv.ParseUint(peer[4], 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP Peer AS format error [%s]", peer[1])
|
||||
}
|
||||
} else {
|
||||
port = 179
|
||||
}
|
||||
|
||||
var mpbgpNexthop, mpbgpIPv4, mpbgpIPv6 string
|
||||
|
||||
if len(config) > 1 {
|
||||
configData := strings.Split(config[1], ";")
|
||||
for _, cfg := range configData {
|
||||
c := strings.Split(cfg, "=")
|
||||
switch c[0] {
|
||||
case "mpbgp_nexthop":
|
||||
mpbgpNexthop = c[1]
|
||||
case "mpbgp_ipv4":
|
||||
mpbgpIPv4 = c[1]
|
||||
case "mpbgp_ipv6":
|
||||
mpbgpIPv6 = c[1]
|
||||
default:
|
||||
return nil, fmt.Errorf("peer configuration parameter '%s' is not supported", c[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peerConfig := BGPPeer{
|
||||
Address: address,
|
||||
AS: uint32(ASNumber),
|
||||
Port: uint16(port),
|
||||
Password: password,
|
||||
MultiHop: multiHop,
|
||||
MpbgpNexthop: mpbgpNexthop,
|
||||
MpbgpIPv4: mpbgpIPv4,
|
||||
MpbgpIPv6: mpbgpIPv6,
|
||||
}
|
||||
|
||||
bgpPeers = append(bgpPeers, peerConfig)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *BGPPeer) FindMpbgpAddresses(ap *api.Peer, server *BGPConfig) (string, string, error) {
|
||||
var ipv4Address, ipv6Address string
|
||||
switch p.MpbgpNexthop {
|
||||
case "fixed":
|
||||
ap.Transport.LocalAddress = server.SourceIP
|
||||
if p.MpbgpIPv4 == "" && p.MpbgpIPv6 == "" {
|
||||
return "", "", fmt.Errorf("to use MP-BGP with fixed address at least one IPv4 or IPv6 address has to be provided [current - IPv4: %s, IPv6: %s]",
|
||||
p.MpbgpIPv4, p.MpbgpIPv6)
|
||||
}
|
||||
|
||||
if p.MpbgpIPv4 != "" {
|
||||
if net.ParseIP(p.MpbgpIPv4) == nil {
|
||||
return "", "", fmt.Errorf("provided address '%s' is not a valid IPv4 address", p.MpbgpIPv4)
|
||||
}
|
||||
}
|
||||
if p.MpbgpIPv6 != "" {
|
||||
if net.ParseIP(p.MpbgpIPv6) == nil {
|
||||
return "", "", fmt.Errorf("provided address '%s' is not a valid IPv6 address", p.MpbgpIPv6)
|
||||
}
|
||||
}
|
||||
|
||||
ipv4Address = p.MpbgpIPv4
|
||||
ipv6Address = p.MpbgpIPv6
|
||||
case "auto_sourceip":
|
||||
ap.Transport.LocalAddress = server.SourceIP
|
||||
|
||||
// Resolve the local interface by SourceIP
|
||||
iface, err := utils.GetInterfaceByIP(server.SourceIP)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get interface by IP: %v", err)
|
||||
}
|
||||
|
||||
if utils.IsIPv4(server.SourceIP) {
|
||||
// Get the non link-local IPv6 address on that interface
|
||||
ipv6Address, err = utils.GetNonLinkLocalIP(iface, netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv6 address: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Get the non link-local IPv4 address on that interface
|
||||
ipv4Address, err = utils.GetNonLinkLocalIP(iface, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv4 address: %v", err)
|
||||
}
|
||||
}
|
||||
case "auto_sourceif":
|
||||
ap.Transport.BindInterface = server.SourceIF
|
||||
|
||||
iface, err := netlink.LinkByName(server.SourceIF)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get interface by name: %v", err)
|
||||
}
|
||||
|
||||
// Get the non link-local IPv4 address on that interface
|
||||
ipv4Address, err = utils.GetNonLinkLocalIP(&iface, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv4 address: %v", err)
|
||||
}
|
||||
|
||||
// Get the non link-local IPv6 address on that interface
|
||||
ipv6Address, err = utils.GetNonLinkLocalIP(&iface, netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv6 address: %v", err)
|
||||
}
|
||||
default:
|
||||
return "", "", fmt.Errorf("option %s for MP-BPG nexthop is not supported", server.MpbgpNexthop)
|
||||
}
|
||||
|
||||
return ipv4Address, ipv6Address, nil
|
||||
}
|
||||
|
||||
func (p *BGPPeer) SetMpbgpOptions(server *BGPConfig) {
|
||||
if p.MpbgpNexthop == "" {
|
||||
p.MpbgpNexthop = server.MpbgpNexthop
|
||||
}
|
||||
|
||||
if p.MpbgpIPv4 == "" {
|
||||
p.MpbgpIPv4 = server.MpbgpIPv4
|
||||
}
|
||||
|
||||
if p.MpbgpIPv6 == "" {
|
||||
p.MpbgpIPv6 = server.MpbgpIPv6
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@ package kubevip
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/bits"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/detector"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// ParseEnvironment - will popultate the configuration from environment variables
|
||||
@@ -16,17 +21,13 @@ func ParseEnvironment(c *Config) error {
|
||||
}
|
||||
// Ensure that logging is set through the environment variables
|
||||
env := os.Getenv(vipLogLevel)
|
||||
// Set default value
|
||||
if env == "" {
|
||||
env = "4"
|
||||
}
|
||||
|
||||
if env != "" {
|
||||
logLevel, err := strconv.ParseUint(env, 10, 32)
|
||||
logLevel, err := strconv.ParseInt(env, 10, 32)
|
||||
if err != nil {
|
||||
panic("Unable to parse environment variable [vip_loglevel], should be int")
|
||||
}
|
||||
c.Logging = int(logLevel)
|
||||
c.Logging = int32(logLevel)
|
||||
}
|
||||
|
||||
// Find interface
|
||||
@@ -35,18 +36,21 @@ func ParseEnvironment(c *Config) error {
|
||||
c.Interface = env
|
||||
}
|
||||
|
||||
env = os.Getenv(vipInterfaceLoGlobal)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.LoInterfaceGlobalScope = b
|
||||
}
|
||||
|
||||
// Find (services) interface
|
||||
env = os.Getenv(vipServicesInterface)
|
||||
if env != "" {
|
||||
c.ServicesInterface = env
|
||||
}
|
||||
|
||||
// Find provider configuration
|
||||
env = os.Getenv(providerConfig)
|
||||
if env != "" {
|
||||
c.ProviderConfig = env
|
||||
}
|
||||
|
||||
// Find Kubernetes Leader Election configuration
|
||||
env = os.Getenv(vipLeaderElection)
|
||||
if env != "" {
|
||||
@@ -100,6 +104,11 @@ func ParseEnvironment(c *Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
env = os.Getenv(nodeName)
|
||||
if env != "" {
|
||||
c.NodeName = env
|
||||
}
|
||||
|
||||
// Find vip address
|
||||
env = os.Getenv(vipAddress)
|
||||
if env != "" {
|
||||
@@ -119,11 +128,11 @@ func ParseEnvironment(c *Config) error {
|
||||
// Find vip port
|
||||
env = os.Getenv(port)
|
||||
if env != "" {
|
||||
i, err := strconv.ParseInt(env, 10, 32)
|
||||
i, err := strconv.ParseUint(env, 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Port = int(i)
|
||||
c.Port = uint16(i)
|
||||
}
|
||||
|
||||
// Find vipDdns
|
||||
@@ -162,6 +171,11 @@ func ParseEnvironment(c *Config) error {
|
||||
c.DetectControlPlane = b
|
||||
}
|
||||
|
||||
env = os.Getenv(kubernetesAddr)
|
||||
if env != "" {
|
||||
c.KubernetesAddr = env
|
||||
}
|
||||
|
||||
// Find Services toggle
|
||||
env = os.Getenv(svcEnable)
|
||||
if env != "" {
|
||||
@@ -192,11 +206,21 @@ func ParseEnvironment(c *Config) error {
|
||||
}
|
||||
|
||||
// Load-balancer class name
|
||||
env = os.Getenv(lbClassName)
|
||||
if env != "" {
|
||||
env, exists := os.LookupEnv(lbClassName)
|
||||
if exists {
|
||||
c.LoadBalancerClassName = env
|
||||
}
|
||||
|
||||
// Load-balancer class legacy handling
|
||||
env = os.Getenv(lbClassLegacyHandling)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.LoadBalancerClassLegacyHandling = b
|
||||
}
|
||||
|
||||
// Find the namespace that the control plane should use (for leaderElection lock)
|
||||
env = os.Getenv(svcNamespace)
|
||||
if env != "" {
|
||||
@@ -210,12 +234,6 @@ func ParseEnvironment(c *Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Find vip address cidr range
|
||||
env = os.Getenv(vipCidr)
|
||||
if env != "" {
|
||||
c.VIPCIDR = env
|
||||
}
|
||||
|
||||
// Find vip address subnet
|
||||
env = os.Getenv(vipSubnet)
|
||||
if env != "" {
|
||||
@@ -273,6 +291,21 @@ func ParseEnvironment(c *Config) error {
|
||||
c.ArpBroadcastRate = 3000
|
||||
}
|
||||
|
||||
// Determine if VIP should be preserved on leadership loss
|
||||
// true: VIP addresses remain on interface, only ARP/NDP broadcasting stops
|
||||
// false (default): VIP addresses are deleted on leadership loss (legacy behavior)
|
||||
env = os.Getenv(vipPreserveOnLeadershipLoss)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.PreserveVIPOnLeadershipLoss = b
|
||||
} else {
|
||||
// Default to false for backward compatibility
|
||||
c.PreserveVIPOnLeadershipLoss = false
|
||||
}
|
||||
|
||||
// Wireguard Mode
|
||||
env = os.Getenv(vipWireguard)
|
||||
if env != "" {
|
||||
@@ -296,11 +329,18 @@ func ParseEnvironment(c *Config) error {
|
||||
// Routing Table ID
|
||||
env = os.Getenv(vipRoutingTableID)
|
||||
if env != "" {
|
||||
i, err := strconv.ParseInt(env, 10, 32)
|
||||
i, err := strconv.ParseInt(env, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.RoutingTableID = int(i)
|
||||
if i >= 0 && i <= math.MaxInt {
|
||||
c.RoutingTableID = int(i)
|
||||
} else if i < 0 {
|
||||
return fmt.Errorf("no support of negative [%d] in env var %q", i, vipRoutingTableID)
|
||||
} else {
|
||||
// +1 for the signing bit as it is 0 for positive integers
|
||||
return fmt.Errorf("no support for int64, system natively supports [int%d]", bits.OnesCount(math.MaxInt)+1)
|
||||
}
|
||||
}
|
||||
|
||||
// Routing Table Type
|
||||
@@ -398,13 +438,31 @@ func ParseEnvironment(c *Config) error {
|
||||
// Peer AS
|
||||
env = os.Getenv(bgpPeers)
|
||||
if env != "" {
|
||||
peers, err := bgp.ParseBGPPeerConfig(env)
|
||||
peers, err := ParseBGPPeerConfig(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.BGPConfig.Peers = peers
|
||||
}
|
||||
|
||||
// MPBGP mode
|
||||
env = os.Getenv(mpbgpNexthop)
|
||||
if env != "" {
|
||||
c.BGPConfig.MpbgpNexthop = env
|
||||
}
|
||||
|
||||
// MPBGP fixed IPv4
|
||||
env = os.Getenv(mpbgpIPv4)
|
||||
if env != "" {
|
||||
c.BGPConfig.MpbgpIPv4 = env
|
||||
}
|
||||
|
||||
// MPBGP fixed IPv6
|
||||
env = os.Getenv(mpbgpIPv6)
|
||||
if env != "" {
|
||||
c.BGPConfig.MpbgpIPv6 = env
|
||||
}
|
||||
|
||||
// BGP Peer mutlihop
|
||||
env = os.Getenv(bgpMultiHop)
|
||||
if env != "" {
|
||||
@@ -459,28 +517,32 @@ func ParseEnvironment(c *Config) error {
|
||||
c.BGPConfig.KeepaliveInterval = u64
|
||||
}
|
||||
|
||||
// Enable the Equinix Metal API calls
|
||||
env = os.Getenv(vipPacket)
|
||||
env = os.Getenv(zebraEnable)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
result, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.EnableMetal = b
|
||||
c.BGPConfig.Zebra.Enabled = result
|
||||
}
|
||||
|
||||
// Find the Equinix Metal project name
|
||||
env = os.Getenv(vipPacketProject)
|
||||
env = os.Getenv(zebraURL)
|
||||
if env != "" {
|
||||
// TODO - parse address net.Host()
|
||||
c.MetalProject = env
|
||||
c.BGPConfig.Zebra.URL = env
|
||||
}
|
||||
|
||||
// Find the Equinix Metal project ID
|
||||
env = os.Getenv(vipPacketProjectID)
|
||||
env = os.Getenv(zebraVersion)
|
||||
if env != "" {
|
||||
// TODO - parse address net.Host()
|
||||
c.MetalProjectID = env
|
||||
u64, err := strconv.ParseUint(env, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.BGPConfig.Zebra.Version = uint32(u64)
|
||||
}
|
||||
|
||||
env = os.Getenv(zebraSoftwareName)
|
||||
if env != "" {
|
||||
c.BGPConfig.Zebra.SoftwareName = env
|
||||
}
|
||||
|
||||
// Enable the load-balancer
|
||||
@@ -496,11 +558,11 @@ func ParseEnvironment(c *Config) error {
|
||||
// Find loadbalancer port
|
||||
env = os.Getenv(lbPort)
|
||||
if env != "" {
|
||||
i, err := strconv.ParseInt(env, 10, 32)
|
||||
i, err := strconv.ParseUint(env, 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.LoadBalancerPort = int(i)
|
||||
c.LoadBalancerPort = uint16(i)
|
||||
}
|
||||
|
||||
// Find loadbalancer forwarding method
|
||||
@@ -561,14 +623,345 @@ func ParseEnvironment(c *Config) error {
|
||||
c.K8sConfigFile = env
|
||||
}
|
||||
|
||||
env = os.Getenv(enableEndpointSlices)
|
||||
env = os.Getenv(enableEndpoints)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.EnableEndpointSlices = b
|
||||
c.EnableEndpoints = b
|
||||
}
|
||||
|
||||
env = os.Getenv(mirrorDestInterface)
|
||||
if env != "" {
|
||||
c.MirrorDestInterface = env
|
||||
}
|
||||
|
||||
env = os.Getenv(iptablesBackend)
|
||||
if env != "" {
|
||||
c.IptablesBackend = env
|
||||
}
|
||||
|
||||
env = os.Getenv(backendHealthCheckInterval)
|
||||
if env != "" {
|
||||
i, err := strconv.ParseInt(env, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.BackendHealthCheckInterval = int(i)
|
||||
}
|
||||
|
||||
env = os.Getenv(healthCheckPort)
|
||||
if env != "" {
|
||||
i, err := strconv.ParseInt(env, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i < 1024 {
|
||||
return fmt.Errorf("health check port should be > 1024")
|
||||
}
|
||||
c.HealthCheckPort = int(i)
|
||||
}
|
||||
|
||||
env = os.Getenv(enableUPNP)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.EnableUPNP = b
|
||||
}
|
||||
|
||||
if env = os.Getenv(egressClean); env == "" {
|
||||
env = os.Getenv(strings.ToUpper(egressClean))
|
||||
}
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.EgressClean = b
|
||||
}
|
||||
|
||||
// check for configuration file path
|
||||
env = os.Getenv(configFile)
|
||||
if env != "" {
|
||||
c.ConfigFile = env
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfigFromFile loads configuration from a JSON or YAML file
|
||||
func LoadConfigFromFile(configFilePath string) (*Config, error) {
|
||||
if configFilePath == "" {
|
||||
return nil, fmt.Errorf("config file path is empty")
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("config file does not exist: %s", configFilePath)
|
||||
}
|
||||
|
||||
// Read file content
|
||||
data, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file %s: %v", configFilePath, err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
ext := strings.ToLower(filepath.Ext(configFilePath))
|
||||
|
||||
switch ext {
|
||||
case ".json":
|
||||
err = json.Unmarshal(data, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON config file %s: %v", configFilePath, err)
|
||||
}
|
||||
case ".yaml", ".yml":
|
||||
err = yaml.Unmarshal(data, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML config file %s: %v", configFilePath, err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported config file format %s. Supported formats: .json, .yaml, .yml", ext)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// MergeConfigFromFile merges configuration loaded from file with existing config
|
||||
// Priority: command line flags > environment variables > config file
|
||||
func MergeConfigFromFile(c *Config, configFilePath string) error {
|
||||
if configFilePath == "" {
|
||||
return nil // No config file specified, nothing to merge
|
||||
}
|
||||
|
||||
fileConfig, err := LoadConfigFromFile(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Merge file config with existing config
|
||||
// Only set values from file if they haven't been set by flags or env vars
|
||||
mergeConfigValues(c, fileConfig)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeConfigValues merges values from fileConfig into baseConfig
|
||||
// Only overwrites zero values in baseConfig
|
||||
func mergeConfigValues(baseConfig, fileConfig *Config) {
|
||||
// Basic configuration
|
||||
if baseConfig.Logging == 0 && fileConfig.Logging != 0 {
|
||||
baseConfig.Logging = fileConfig.Logging
|
||||
}
|
||||
|
||||
// Network configuration
|
||||
if baseConfig.Interface == "" && fileConfig.Interface != "" {
|
||||
baseConfig.Interface = fileConfig.Interface
|
||||
}
|
||||
if baseConfig.ServicesInterface == "" && fileConfig.ServicesInterface != "" {
|
||||
baseConfig.ServicesInterface = fileConfig.ServicesInterface
|
||||
}
|
||||
if baseConfig.VIP == "" && fileConfig.VIP != "" {
|
||||
baseConfig.VIP = fileConfig.VIP
|
||||
}
|
||||
if baseConfig.VIPSubnet == "" && fileConfig.VIPSubnet != "" {
|
||||
baseConfig.VIPSubnet = fileConfig.VIPSubnet
|
||||
}
|
||||
if baseConfig.Address == "" && fileConfig.Address != "" {
|
||||
baseConfig.Address = fileConfig.Address
|
||||
}
|
||||
if baseConfig.Port == 0 && fileConfig.Port != 0 {
|
||||
baseConfig.Port = fileConfig.Port
|
||||
}
|
||||
if baseConfig.NodeName == "" && fileConfig.NodeName != "" {
|
||||
baseConfig.NodeName = fileConfig.NodeName
|
||||
}
|
||||
|
||||
// Boolean flags - only merge if not explicitly set
|
||||
if !baseConfig.EnableARP && fileConfig.EnableARP {
|
||||
baseConfig.EnableARP = fileConfig.EnableARP
|
||||
}
|
||||
if !baseConfig.EnableBGP && fileConfig.EnableBGP {
|
||||
baseConfig.EnableBGP = fileConfig.EnableBGP
|
||||
}
|
||||
if !baseConfig.EnableWireguard && fileConfig.EnableWireguard {
|
||||
baseConfig.EnableWireguard = fileConfig.EnableWireguard
|
||||
}
|
||||
if !baseConfig.EnableRoutingTable && fileConfig.EnableRoutingTable {
|
||||
baseConfig.EnableRoutingTable = fileConfig.EnableRoutingTable
|
||||
}
|
||||
if !baseConfig.EnableControlPlane && fileConfig.EnableControlPlane {
|
||||
baseConfig.EnableControlPlane = fileConfig.EnableControlPlane
|
||||
}
|
||||
if !baseConfig.DetectControlPlane && fileConfig.DetectControlPlane {
|
||||
baseConfig.DetectControlPlane = fileConfig.DetectControlPlane
|
||||
}
|
||||
if !baseConfig.EnableServices && fileConfig.EnableServices {
|
||||
baseConfig.EnableServices = fileConfig.EnableServices
|
||||
}
|
||||
if !baseConfig.EnableServicesElection && fileConfig.EnableServicesElection {
|
||||
baseConfig.EnableServicesElection = fileConfig.EnableServicesElection
|
||||
}
|
||||
if !baseConfig.EnableNodeLabeling && fileConfig.EnableNodeLabeling {
|
||||
baseConfig.EnableNodeLabeling = fileConfig.EnableNodeLabeling
|
||||
}
|
||||
if !baseConfig.EnableLoadBalancer && fileConfig.EnableLoadBalancer {
|
||||
baseConfig.EnableLoadBalancer = fileConfig.EnableLoadBalancer
|
||||
}
|
||||
if !baseConfig.DDNS && fileConfig.DDNS {
|
||||
baseConfig.DDNS = fileConfig.DDNS
|
||||
}
|
||||
if !baseConfig.SingleNode && fileConfig.SingleNode {
|
||||
baseConfig.SingleNode = fileConfig.SingleNode
|
||||
}
|
||||
if !baseConfig.StartAsLeader && fileConfig.StartAsLeader {
|
||||
baseConfig.StartAsLeader = fileConfig.StartAsLeader
|
||||
}
|
||||
if !baseConfig.PreserveVIPOnLeadershipLoss && fileConfig.PreserveVIPOnLeadershipLoss {
|
||||
baseConfig.PreserveVIPOnLeadershipLoss = fileConfig.PreserveVIPOnLeadershipLoss
|
||||
}
|
||||
|
||||
// Service configuration
|
||||
if baseConfig.Namespace == "" && fileConfig.Namespace != "" {
|
||||
baseConfig.Namespace = fileConfig.Namespace
|
||||
}
|
||||
if baseConfig.ServiceNamespace == "" && fileConfig.ServiceNamespace != "" {
|
||||
baseConfig.ServiceNamespace = fileConfig.ServiceNamespace
|
||||
}
|
||||
if baseConfig.ServicesLeaseName == "" && fileConfig.ServicesLeaseName != "" {
|
||||
baseConfig.ServicesLeaseName = fileConfig.ServicesLeaseName
|
||||
}
|
||||
|
||||
// LoadBalancer configuration
|
||||
if baseConfig.LoadBalancerPort == 0 && fileConfig.LoadBalancerPort != 0 {
|
||||
baseConfig.LoadBalancerPort = fileConfig.LoadBalancerPort
|
||||
}
|
||||
if baseConfig.LoadBalancerForwardingMethod == "" && fileConfig.LoadBalancerForwardingMethod != "" {
|
||||
baseConfig.LoadBalancerForwardingMethod = fileConfig.LoadBalancerForwardingMethod
|
||||
}
|
||||
if baseConfig.LoadBalancerClassName == "" && fileConfig.LoadBalancerClassName != "" {
|
||||
baseConfig.LoadBalancerClassName = fileConfig.LoadBalancerClassName
|
||||
}
|
||||
|
||||
// Routing Table configuration
|
||||
if baseConfig.RoutingTableID == 0 && fileConfig.RoutingTableID != 0 {
|
||||
baseConfig.RoutingTableID = fileConfig.RoutingTableID
|
||||
}
|
||||
if baseConfig.RoutingTableType == 0 && fileConfig.RoutingTableType != 0 {
|
||||
baseConfig.RoutingTableType = fileConfig.RoutingTableType
|
||||
}
|
||||
if baseConfig.RoutingProtocol == 0 && fileConfig.RoutingProtocol != 0 {
|
||||
baseConfig.RoutingProtocol = fileConfig.RoutingProtocol
|
||||
}
|
||||
|
||||
// BGP configuration
|
||||
mergeBGPConfig(&baseConfig.BGPConfig, &fileConfig.BGPConfig)
|
||||
|
||||
// Kubernetes configuration
|
||||
if baseConfig.K8sConfigFile == "" && fileConfig.K8sConfigFile != "" {
|
||||
baseConfig.K8sConfigFile = fileConfig.K8sConfigFile
|
||||
}
|
||||
|
||||
// Leader Election configuration
|
||||
mergeLeaderElectionConfig(&baseConfig.KubernetesLeaderElection, &fileConfig.KubernetesLeaderElection)
|
||||
|
||||
// Prometheus configuration
|
||||
if baseConfig.PrometheusHTTPServer == "" && fileConfig.PrometheusHTTPServer != "" {
|
||||
baseConfig.PrometheusHTTPServer = fileConfig.PrometheusHTTPServer
|
||||
}
|
||||
|
||||
// DNS configuration
|
||||
if baseConfig.DNSMode == "" && fileConfig.DNSMode != "" {
|
||||
baseConfig.DNSMode = fileConfig.DNSMode
|
||||
}
|
||||
|
||||
// Health check configuration
|
||||
if baseConfig.HealthCheckPort == 0 && fileConfig.HealthCheckPort != 0 {
|
||||
baseConfig.HealthCheckPort = fileConfig.HealthCheckPort
|
||||
}
|
||||
|
||||
// Egress configuration
|
||||
if baseConfig.EgressPodCidr == "" && fileConfig.EgressPodCidr != "" {
|
||||
baseConfig.EgressPodCidr = fileConfig.EgressPodCidr
|
||||
}
|
||||
if baseConfig.EgressServiceCidr == "" && fileConfig.EgressServiceCidr != "" {
|
||||
baseConfig.EgressServiceCidr = fileConfig.EgressServiceCidr
|
||||
}
|
||||
|
||||
// Mirror configuration
|
||||
if baseConfig.MirrorDestInterface == "" && fileConfig.MirrorDestInterface != "" {
|
||||
baseConfig.MirrorDestInterface = fileConfig.MirrorDestInterface
|
||||
}
|
||||
|
||||
// Iptables configuration
|
||||
if baseConfig.IptablesBackend == "" && fileConfig.IptablesBackend != "" {
|
||||
baseConfig.IptablesBackend = fileConfig.IptablesBackend
|
||||
}
|
||||
|
||||
// Backend health check interval
|
||||
if baseConfig.BackendHealthCheckInterval == 0 && fileConfig.BackendHealthCheckInterval != 0 {
|
||||
baseConfig.BackendHealthCheckInterval = fileConfig.BackendHealthCheckInterval
|
||||
}
|
||||
|
||||
// ARP broadcast rate
|
||||
if baseConfig.ArpBroadcastRate == 0 && fileConfig.ArpBroadcastRate != 0 {
|
||||
baseConfig.ArpBroadcastRate = fileConfig.ArpBroadcastRate
|
||||
}
|
||||
|
||||
// Annotations
|
||||
if baseConfig.Annotations == "" && fileConfig.Annotations != "" {
|
||||
baseConfig.Annotations = fileConfig.Annotations
|
||||
}
|
||||
|
||||
// Load balancers slice
|
||||
if len(baseConfig.LoadBalancers) == 0 && len(fileConfig.LoadBalancers) > 0 {
|
||||
baseConfig.LoadBalancers = fileConfig.LoadBalancers
|
||||
}
|
||||
}
|
||||
|
||||
// mergeBGPConfig merges BGP configuration
|
||||
func mergeBGPConfig(base, file *BGPConfig) {
|
||||
if base.RouterID == "" && file.RouterID != "" {
|
||||
base.RouterID = file.RouterID
|
||||
}
|
||||
if base.AS == 0 && file.AS != 0 {
|
||||
base.AS = file.AS
|
||||
}
|
||||
if base.SourceIF == "" && file.SourceIF != "" {
|
||||
base.SourceIF = file.SourceIF
|
||||
}
|
||||
if base.SourceIP == "" && file.SourceIP != "" {
|
||||
base.SourceIP = file.SourceIP
|
||||
}
|
||||
if base.HoldTime == 0 && file.HoldTime != 0 {
|
||||
base.HoldTime = file.HoldTime
|
||||
}
|
||||
if base.KeepaliveInterval == 0 && file.KeepaliveInterval != 0 {
|
||||
base.KeepaliveInterval = file.KeepaliveInterval
|
||||
}
|
||||
if len(base.Peers) == 0 && len(file.Peers) > 0 {
|
||||
base.Peers = file.Peers
|
||||
}
|
||||
}
|
||||
|
||||
// mergeLeaderElectionConfig merges leader election configuration
|
||||
func mergeLeaderElectionConfig(base, file *KubernetesLeaderElection) {
|
||||
if base.LeaseName == "" && file.LeaseName != "" {
|
||||
base.LeaseName = file.LeaseName
|
||||
}
|
||||
if base.LeaseDuration == 0 && file.LeaseDuration != 0 {
|
||||
base.LeaseDuration = file.LeaseDuration
|
||||
}
|
||||
if base.RenewDeadline == 0 && file.RenewDeadline != 0 {
|
||||
base.RenewDeadline = file.RenewDeadline
|
||||
}
|
||||
if base.RetryPeriod == 0 && file.RetryPeriod != 0 {
|
||||
base.RetryPeriod = file.RetryPeriod
|
||||
}
|
||||
if len(base.LeaseAnnotations) == 0 && len(file.LeaseAnnotations) > 0 {
|
||||
base.LeaseAnnotations = file.LeaseAnnotations
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ const (
|
||||
// vip_arpRate - defines the rate of gARP broadcasts
|
||||
vipArpRate = "vip_arpRate"
|
||||
|
||||
// vipPreserveOnLeadershipLoss - if true, VIP addresses will remain on interface when leadership is lost
|
||||
vipPreserveOnLeadershipLoss = "vip_preserve_on_leadership_loss"
|
||||
|
||||
// vipLeaderElection - defines if the kubernetes algorithm should be used
|
||||
vipLeaderElection = "vip_leaderelection"
|
||||
|
||||
@@ -33,12 +36,12 @@ const (
|
||||
// vipInterface - defines the interface that the vip should bind too
|
||||
vipInterface = "vip_interface"
|
||||
|
||||
// vipInterfaceLoGlobal - defines if the lo interface (if used) should have a global scope
|
||||
vipInterfaceLoGlobal = "vip_interfaceloglobal"
|
||||
|
||||
// vipServicesInterface - defines the interface that the service vips should bind too
|
||||
vipServicesInterface = "vip_servicesinterface"
|
||||
|
||||
// vipCidr - defines the cidr that the vip will use (for BGP)
|
||||
vipCidr = "vip_cidr"
|
||||
|
||||
// vipSubnet - defines the subnet that the vip will use
|
||||
vipSubnet = "vip_subnet"
|
||||
|
||||
@@ -74,24 +77,15 @@ const (
|
||||
// vipDdns - defines if use dynamic dns to allocate IP for "address"
|
||||
vipDdns = "vip_ddns"
|
||||
|
||||
// vipLeaseNodeName defines the node name that is used to acquire leases
|
||||
nodeName = "vip_nodename"
|
||||
|
||||
// vipSingleNode - defines the vip start as a single node cluster
|
||||
vipSingleNode = "vip_singlenode"
|
||||
|
||||
// vipStartLeader - will start this instance as the leader of the cluster
|
||||
vipStartLeader = "vip_startleader"
|
||||
|
||||
// vipPacket defines that the packet API will be used for EIP
|
||||
vipPacket = "vip_packet"
|
||||
|
||||
// vipPacketProject defines which project within Packet to use
|
||||
vipPacketProject = "vip_packetproject"
|
||||
|
||||
// vipPacketProjectID defines which projectID within Packet to use
|
||||
vipPacketProjectID = "vip_packetprojectid"
|
||||
|
||||
// providerConfig defines a path to a configuration that should be parsed
|
||||
providerConfig = "provider_config"
|
||||
|
||||
// bgpEnable defines if BGP should be enabled
|
||||
bgpEnable = "bgp_enable"
|
||||
// bgpRouterID defines the routerID for the BGP server
|
||||
@@ -119,6 +113,22 @@ const (
|
||||
// bgpKeepaliveInterval defines bgp timers keepalive interval
|
||||
bgpKeepaliveInterval = "bgp_keepalive_interval"
|
||||
|
||||
// zebraEnable defines if Zebra integraton should be enabled
|
||||
zebraEnable = "zebra_enable"
|
||||
// zebraUrl specifies path to the unix domain socket for connecting to Zebra daemon
|
||||
zebraURL = "zebra_url"
|
||||
// zebraVersion specifies Zebra API Version
|
||||
zebraVersion = "zebra_version"
|
||||
// zebraSoftwareName specifies Software Name for Zebra
|
||||
zebraSoftwareName = "zebra_software_name"
|
||||
|
||||
// mpbgpNexthop defines MPBGP mode
|
||||
mpbgpNexthop = "mpbgp_nexthop"
|
||||
// mpbgpIPv4 defines fixed IPv4 to be used with MPBGP
|
||||
mpbgpIPv4 = "mpbgp_ipv4"
|
||||
// mpbgpIPv6 defines fixed IPv6 to be used with MPBGP
|
||||
mpbgpIPv6 = "mpbgp_ipv6"
|
||||
|
||||
// vipWireguard - defines if wireguard will be used for vips
|
||||
vipWireguard = "vip_wireguard" //nolint
|
||||
|
||||
@@ -151,6 +161,9 @@ const (
|
||||
// cpDetect will attempt to automatically find a working address for the control plane from loopback
|
||||
cpDetect = "cp_detect"
|
||||
|
||||
// kubernetesAddr,is the address of the Kubernetes API server on this machine
|
||||
kubernetesAddr = "kubernetes_addr"
|
||||
|
||||
// svcEnable enables the Kubernetes service feature
|
||||
svcEnable = "svc_enable"
|
||||
|
||||
@@ -169,6 +182,9 @@ const (
|
||||
// lbClassName enables load-balancer for a specific class only
|
||||
lbClassName = "lb_class_name"
|
||||
|
||||
// lbClassLegacyHandling enables legacy handing of load-balancer class
|
||||
lbClassLegacyHandling = "lb_class_legacy_handling"
|
||||
|
||||
// lbEnable defines if the load-balancer should be enabled
|
||||
lbEnable = "lb_enable"
|
||||
|
||||
@@ -190,7 +206,7 @@ const (
|
||||
// vipConfigMap defines the configmap that kube-vip will watch for service definitions
|
||||
// vipConfigMap = "vip_configmap"
|
||||
|
||||
//k8sConfigFile defines the path to the configfile used to speak with the API server
|
||||
// k8sConfigFile defines the path to the configfile used to speak with the API server
|
||||
k8sConfigFile = "k8s_config_file"
|
||||
|
||||
// dnsMode defines mode that DNS lookup will be performed with (first, ipv4, ipv6, dual)
|
||||
@@ -199,6 +215,29 @@ const (
|
||||
// disableServiceUpdates disables service updating
|
||||
disableServiceUpdates = "disable_service_updates"
|
||||
|
||||
// enableEndpointSlices enables use of EndpointSlices instead of Endpoints
|
||||
enableEndpointSlices = "enable_endpointslices"
|
||||
// enableEndpoints enables use of Endpoints instead of EndpointSlices
|
||||
enableEndpoints = "enable_endpoints"
|
||||
|
||||
// mirrorDestInterface is the network interface where all traffics that go through service interface
|
||||
// will be mirrored to. The source interface is ServicesInterface by default, fall back to Interface if not set.
|
||||
// + optional
|
||||
mirrorDestInterface = "mirror_dest_interface"
|
||||
|
||||
// iptablesBackend iptables backend, can be specified as `nft` or `legacy`. If not set, it defaults to automatic detection.
|
||||
iptablesBackend = "iptables_backend"
|
||||
|
||||
// backendHealthCheckInterval Interval in seconds for checking backend health.
|
||||
backendHealthCheckInterval = "backend_health_check_interval"
|
||||
|
||||
// healthCheckPort, if set to non-zero will be the port the health check will listen on
|
||||
healthCheckPort = "health_check_port"
|
||||
|
||||
// enableUPNP enables UPNP functions
|
||||
enableUPNP = "enable_upnp"
|
||||
|
||||
// egressClean enables egress cleaning on kube-vip's start
|
||||
egressClean = "egress_clean"
|
||||
|
||||
// configFile defines the path to a JSON/YAML configuration file
|
||||
configFile = "config_file"
|
||||
)
|
||||
|
||||
559
pkg/kubevip/config_file_test.go
Normal file
559
pkg/kubevip/config_file_test.go
Normal file
@@ -0,0 +1,559 @@
|
||||
package kubevip
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfigFromFile(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tmpDir, err := os.MkdirTemp("", "kube-vip-config-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
content string
|
||||
expectedConfig *Config
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "Valid YAML config",
|
||||
filename: "config.yaml",
|
||||
content: `
|
||||
logging: 2
|
||||
enableARP: true
|
||||
enableControlPlane: true
|
||||
enableServices: true
|
||||
address: "192.168.1.100"
|
||||
port: 6443
|
||||
interface: "eth0"
|
||||
namespace: "kube-system"
|
||||
vipSubnet: "192.168.1.0/24"
|
||||
leaseName: "test-lease"
|
||||
leaseDuration: 15
|
||||
renewDeadline: 10
|
||||
retryPeriod: 2
|
||||
prometheusHTTPServer: ":2112"
|
||||
`,
|
||||
expectedConfig: &Config{
|
||||
Logging: 2,
|
||||
EnableARP: true,
|
||||
EnableControlPlane: true,
|
||||
EnableServices: true,
|
||||
Address: "192.168.1.100",
|
||||
Port: 6443,
|
||||
Interface: "eth0",
|
||||
Namespace: "kube-system",
|
||||
VIPSubnet: "192.168.1.0/24",
|
||||
PrometheusHTTPServer: ":2112",
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "test-lease",
|
||||
LeaseDuration: 15,
|
||||
RenewDeadline: 10,
|
||||
RetryPeriod: 2,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid JSON config",
|
||||
filename: "config.json",
|
||||
content: `{
|
||||
"logging": 3,
|
||||
"enableBGP": true,
|
||||
"enableServices": true,
|
||||
"address": "10.0.0.100",
|
||||
"port": 8443,
|
||||
"interface": "ens192",
|
||||
"namespace": "kube-system",
|
||||
"loadBalancers": [
|
||||
{
|
||||
"name": "control-plane",
|
||||
"ports": [
|
||||
{
|
||||
"type": "TCP",
|
||||
"port": 6443
|
||||
}
|
||||
],
|
||||
"bindToVip": true,
|
||||
"forwardingMethod": "local"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expectedConfig: &Config{
|
||||
Logging: 3,
|
||||
EnableBGP: true,
|
||||
EnableServices: true,
|
||||
Address: "10.0.0.100",
|
||||
Port: 8443,
|
||||
Interface: "ens192",
|
||||
Namespace: "kube-system",
|
||||
LoadBalancers: []LoadBalancer{
|
||||
{
|
||||
Name: "control-plane",
|
||||
Ports: []Port{
|
||||
{
|
||||
Type: "TCP",
|
||||
Port: 6443,
|
||||
},
|
||||
},
|
||||
BindToVip: true,
|
||||
ForwardingMethod: "local",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Complex BGP config",
|
||||
filename: "bgp-config.yaml",
|
||||
content: `
|
||||
enableBGP: true
|
||||
bgpConfig:
|
||||
routerID: "192.168.1.1"
|
||||
as: 65000
|
||||
sourceIF: "eth0"
|
||||
holdTime: 60
|
||||
keepaliveInterval: 20
|
||||
peers:
|
||||
- address: "192.168.1.2"
|
||||
as: 65001
|
||||
port: 179
|
||||
multiHop: false
|
||||
- address: "192.168.1.3"
|
||||
as: 65002
|
||||
port: 179
|
||||
multiHop: true
|
||||
`,
|
||||
expectedConfig: &Config{
|
||||
EnableBGP: true,
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "192.168.1.1",
|
||||
AS: 65000,
|
||||
SourceIF: "eth0",
|
||||
HoldTime: 60,
|
||||
KeepaliveInterval: 20,
|
||||
Peers: []BGPPeer{
|
||||
{
|
||||
Address: "192.168.1.2",
|
||||
AS: 65001,
|
||||
Port: 179,
|
||||
MultiHop: false,
|
||||
},
|
||||
{
|
||||
Address: "192.168.1.3",
|
||||
AS: 65002,
|
||||
Port: 179,
|
||||
MultiHop: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
filename: "invalid.json",
|
||||
content: `{"logging": 2, "invalid": }`,
|
||||
wantErr: true,
|
||||
errContains: "failed to parse JSON config file",
|
||||
},
|
||||
{
|
||||
name: "Invalid YAML",
|
||||
filename: "invalid.yaml",
|
||||
content: "logging: 2\ninvalid: [unclosed",
|
||||
wantErr: true,
|
||||
errContains: "failed to parse YAML config file",
|
||||
},
|
||||
{
|
||||
name: "Unsupported format",
|
||||
filename: "config.txt",
|
||||
content: "logging=2",
|
||||
wantErr: true,
|
||||
errContains: "unsupported config file format",
|
||||
},
|
||||
{
|
||||
name: "Empty path",
|
||||
filename: "",
|
||||
content: "",
|
||||
wantErr: true,
|
||||
errContains: "config file path is empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var filePath string
|
||||
if tt.filename != "" {
|
||||
filePath = filepath.Join(tmpDir, tt.filename)
|
||||
if err := os.WriteFile(filePath, []byte(tt.content), 0600); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
config, err := LoadConfigFromFile(filePath)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("LoadConfigFromFile() expected error, got nil")
|
||||
return
|
||||
}
|
||||
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
|
||||
t.Errorf("LoadConfigFromFile() error = %v, expected to contain %v", err, tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("LoadConfigFromFile() unexpected error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
t.Errorf("LoadConfigFromFile() returned nil config")
|
||||
return
|
||||
}
|
||||
|
||||
// Compare key fields
|
||||
if config.Logging != tt.expectedConfig.Logging {
|
||||
t.Errorf("Logging = %v, expected %v", config.Logging, tt.expectedConfig.Logging)
|
||||
}
|
||||
if config.EnableARP != tt.expectedConfig.EnableARP {
|
||||
t.Errorf("EnableARP = %v, expected %v", config.EnableARP, tt.expectedConfig.EnableARP)
|
||||
}
|
||||
if config.EnableBGP != tt.expectedConfig.EnableBGP {
|
||||
t.Errorf("EnableBGP = %v, expected %v", config.EnableBGP, tt.expectedConfig.EnableBGP)
|
||||
}
|
||||
if config.Address != tt.expectedConfig.Address {
|
||||
t.Errorf("Address = %v, expected %v", config.Address, tt.expectedConfig.Address)
|
||||
}
|
||||
if config.Port != tt.expectedConfig.Port {
|
||||
t.Errorf("Port = %v, expected %v", config.Port, tt.expectedConfig.Port)
|
||||
}
|
||||
if config.Interface != tt.expectedConfig.Interface {
|
||||
t.Errorf("Interface = %v, expected %v", config.Interface, tt.expectedConfig.Interface)
|
||||
}
|
||||
|
||||
// Test BGP config if present
|
||||
if tt.expectedConfig.EnableBGP {
|
||||
if config.BGPConfig.RouterID != tt.expectedConfig.BGPConfig.RouterID {
|
||||
t.Errorf("BGPConfig.RouterID = %v, expected %v", config.BGPConfig.RouterID, tt.expectedConfig.BGPConfig.RouterID)
|
||||
}
|
||||
if config.BGPConfig.AS != tt.expectedConfig.BGPConfig.AS {
|
||||
t.Errorf("BGPConfig.AS = %v, expected %v", config.BGPConfig.AS, tt.expectedConfig.BGPConfig.AS)
|
||||
}
|
||||
if len(config.BGPConfig.Peers) != len(tt.expectedConfig.BGPConfig.Peers) {
|
||||
t.Errorf("BGPConfig.Peers length = %v, expected %v", len(config.BGPConfig.Peers), len(tt.expectedConfig.BGPConfig.Peers))
|
||||
}
|
||||
}
|
||||
|
||||
// Test LoadBalancers if present
|
||||
if len(tt.expectedConfig.LoadBalancers) > 0 {
|
||||
if len(config.LoadBalancers) != len(tt.expectedConfig.LoadBalancers) {
|
||||
t.Errorf("LoadBalancers length = %v, expected %v", len(config.LoadBalancers), len(tt.expectedConfig.LoadBalancers))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfigFromFile(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tmpDir, err := os.MkdirTemp("", "kube-vip-merge-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create test config file
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
configContent := `
|
||||
logging: 3
|
||||
enableARP: true
|
||||
enableServices: true
|
||||
address: "192.168.1.200"
|
||||
port: 6443
|
||||
interface: "eth1"
|
||||
namespace: "test-namespace"
|
||||
leaseName: "file-lease"
|
||||
leaseDuration: 20
|
||||
prometheusHTTPServer: ":3000"
|
||||
`
|
||||
if err := os.WriteFile(configFile, []byte(configContent), 0600); err != nil {
|
||||
t.Fatalf("Failed to write test config file: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
baseConfig *Config
|
||||
configFilePath string
|
||||
expected *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Merge with empty base config",
|
||||
baseConfig: &Config{},
|
||||
configFilePath: configFile,
|
||||
expected: &Config{
|
||||
Logging: 3,
|
||||
EnableARP: true,
|
||||
EnableServices: true,
|
||||
Address: "192.168.1.200",
|
||||
Port: 6443,
|
||||
Interface: "eth1",
|
||||
Namespace: "test-namespace",
|
||||
PrometheusHTTPServer: ":3000",
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "file-lease",
|
||||
LeaseDuration: 20,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Merge respects existing values (priority test)",
|
||||
baseConfig: &Config{
|
||||
Logging: 5, // Should not be overridden
|
||||
Port: 8443, // Should not be overridden
|
||||
Interface: "eth0", // Should not be overridden
|
||||
},
|
||||
configFilePath: configFile,
|
||||
expected: &Config{
|
||||
Logging: 5, // From base (higher priority)
|
||||
EnableARP: true, // From file
|
||||
EnableServices: true, // From file
|
||||
Address: "192.168.1.200", // From file
|
||||
Port: 8443, // From base (higher priority)
|
||||
Interface: "eth0", // From base (higher priority)
|
||||
Namespace: "test-namespace", // From file
|
||||
PrometheusHTTPServer: ":3000", // From file
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "file-lease", // From file
|
||||
LeaseDuration: 20, // From file
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty config file path",
|
||||
baseConfig: &Config{
|
||||
Logging: 1,
|
||||
},
|
||||
configFilePath: "",
|
||||
expected: &Config{
|
||||
Logging: 1, // Unchanged
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Non-existent config file",
|
||||
baseConfig: &Config{
|
||||
Logging: 1,
|
||||
},
|
||||
configFilePath: "/non/existent/file.yaml",
|
||||
expected: &Config{
|
||||
Logging: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := MergeConfigFromFile(tt.baseConfig, tt.configFilePath)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("MergeConfigFromFile() expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("MergeConfigFromFile() unexpected error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Compare key fields
|
||||
if tt.baseConfig.Logging != tt.expected.Logging {
|
||||
t.Errorf("Logging = %v, expected %v", tt.baseConfig.Logging, tt.expected.Logging)
|
||||
}
|
||||
if tt.baseConfig.EnableARP != tt.expected.EnableARP {
|
||||
t.Errorf("EnableARP = %v, expected %v", tt.baseConfig.EnableARP, tt.expected.EnableARP)
|
||||
}
|
||||
if tt.baseConfig.Address != tt.expected.Address {
|
||||
t.Errorf("Address = %v, expected %v", tt.baseConfig.Address, tt.expected.Address)
|
||||
}
|
||||
if tt.baseConfig.Port != tt.expected.Port {
|
||||
t.Errorf("Port = %v, expected %v", tt.baseConfig.Port, tt.expected.Port)
|
||||
}
|
||||
if tt.baseConfig.Interface != tt.expected.Interface {
|
||||
t.Errorf("Interface = %v, expected %v", tt.baseConfig.Interface, tt.expected.Interface)
|
||||
}
|
||||
if tt.baseConfig.Namespace != tt.expected.Namespace {
|
||||
t.Errorf("Namespace = %v, expected %v", tt.baseConfig.Namespace, tt.expected.Namespace)
|
||||
}
|
||||
if tt.baseConfig.PrometheusHTTPServer != tt.expected.PrometheusHTTPServer {
|
||||
t.Errorf("PrometheusHTTPServer = %v, expected %v", tt.baseConfig.PrometheusHTTPServer, tt.expected.PrometheusHTTPServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfigValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseConfig *Config
|
||||
fileConfig *Config
|
||||
expectedBase *Config
|
||||
}{
|
||||
{
|
||||
name: "Merge basic configuration",
|
||||
baseConfig: &Config{
|
||||
Logging: 5, // Should not be overridden
|
||||
Port: 0, // Should be overridden
|
||||
},
|
||||
fileConfig: &Config{
|
||||
Logging: 2,
|
||||
Port: 6443,
|
||||
Interface: "eth0",
|
||||
Address: "192.168.1.100",
|
||||
},
|
||||
expectedBase: &Config{
|
||||
Logging: 5, // From base (non-zero)
|
||||
Port: 6443, // From file (base was zero)
|
||||
Interface: "eth0", // From file (base was empty)
|
||||
Address: "192.168.1.100", // From file (base was empty)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Merge boolean flags",
|
||||
baseConfig: &Config{
|
||||
EnableARP: true, // Should not be overridden
|
||||
},
|
||||
fileConfig: &Config{
|
||||
EnableARP: false, // Should not override true
|
||||
EnableBGP: true, // Should be set
|
||||
EnableServices: true, // Should be set
|
||||
EnableWireguard: false, // Should not be set (false doesn't override false)
|
||||
},
|
||||
expectedBase: &Config{
|
||||
EnableARP: true, // From base (true has priority)
|
||||
EnableBGP: true, // From file
|
||||
EnableServices: true, // From file
|
||||
EnableWireguard: false, // Remains false
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Merge BGP configuration",
|
||||
baseConfig: &Config{
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "1.1.1.1", // Should not be overridden
|
||||
},
|
||||
},
|
||||
fileConfig: &Config{
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "2.2.2.2", // Should not override
|
||||
AS: 65000, // Should be set
|
||||
SourceIF: "eth0", // Should be set
|
||||
HoldTime: 30, // Should be set
|
||||
KeepaliveInterval: 10, // Should be set
|
||||
},
|
||||
},
|
||||
expectedBase: &Config{
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "1.1.1.1", // From base (non-empty)
|
||||
AS: 65000, // From file (base was zero)
|
||||
SourceIF: "eth0", // From file (base was empty)
|
||||
HoldTime: 30, // From file (base was zero)
|
||||
KeepaliveInterval: 10, // From file (base was zero)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Merge leader election configuration",
|
||||
baseConfig: &Config{
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "base-lease", // Should not be overridden
|
||||
},
|
||||
},
|
||||
fileConfig: &Config{
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "file-lease", // Should not override
|
||||
LeaseDuration: 15, // Should be set
|
||||
RenewDeadline: 10, // Should be set
|
||||
RetryPeriod: 2, // Should be set
|
||||
},
|
||||
},
|
||||
expectedBase: &Config{
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "base-lease", // From base (non-empty)
|
||||
LeaseDuration: 15, // From file (base was zero)
|
||||
RenewDeadline: 10, // From file (base was zero)
|
||||
RetryPeriod: 2, // From file (base was zero)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mergeConfigValues(tt.baseConfig, tt.fileConfig)
|
||||
|
||||
// Compare results
|
||||
if tt.baseConfig.Logging != tt.expectedBase.Logging {
|
||||
t.Errorf("Logging = %v, expected %v", tt.baseConfig.Logging, tt.expectedBase.Logging)
|
||||
}
|
||||
if tt.baseConfig.Port != tt.expectedBase.Port {
|
||||
t.Errorf("Port = %v, expected %v", tt.baseConfig.Port, tt.expectedBase.Port)
|
||||
}
|
||||
if tt.baseConfig.Interface != tt.expectedBase.Interface {
|
||||
t.Errorf("Interface = %v, expected %v", tt.baseConfig.Interface, tt.expectedBase.Interface)
|
||||
}
|
||||
if tt.baseConfig.EnableARP != tt.expectedBase.EnableARP {
|
||||
t.Errorf("EnableARP = %v, expected %v", tt.baseConfig.EnableARP, tt.expectedBase.EnableARP)
|
||||
}
|
||||
if tt.baseConfig.EnableBGP != tt.expectedBase.EnableBGP {
|
||||
t.Errorf("EnableBGP = %v, expected %v", tt.baseConfig.EnableBGP, tt.expectedBase.EnableBGP)
|
||||
}
|
||||
if tt.baseConfig.BGPConfig.RouterID != tt.expectedBase.BGPConfig.RouterID {
|
||||
t.Errorf("BGPConfig.RouterID = %v, expected %v", tt.baseConfig.BGPConfig.RouterID, tt.expectedBase.BGPConfig.RouterID)
|
||||
}
|
||||
if tt.baseConfig.BGPConfig.AS != tt.expectedBase.BGPConfig.AS {
|
||||
t.Errorf("BGPConfig.AS = %v, expected %v", tt.baseConfig.BGPConfig.AS, tt.expectedBase.BGPConfig.AS)
|
||||
}
|
||||
if tt.baseConfig.KubernetesLeaderElection.LeaseName != tt.expectedBase.KubernetesLeaderElection.LeaseName {
|
||||
t.Errorf("KubernetesLeaderElection.LeaseName = %v, expected %v", tt.baseConfig.KubernetesLeaderElection.LeaseName, tt.expectedBase.KubernetesLeaderElection.LeaseName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromFile_FileNotExists(t *testing.T) {
|
||||
_, err := LoadConfigFromFile("/non/existent/path/config.yaml")
|
||||
if err == nil {
|
||||
t.Error("LoadConfigFromFile() expected error for non-existent file, got nil")
|
||||
}
|
||||
if !containsString(err.Error(), "config file does not exist") {
|
||||
t.Errorf("LoadConfigFromFile() error = %v, expected to contain 'config file does not exist'", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a string contains a substring
|
||||
func containsString(str, substr string) bool {
|
||||
return len(str) >= len(substr) && (str == substr || len(substr) == 0 ||
|
||||
(len(substr) > 0 && func() bool {
|
||||
for i := 0; i <= len(str)-len(substr); i++ {
|
||||
if str[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}()))
|
||||
}
|
||||
@@ -2,23 +2,58 @@ package kubevip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
appv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
applyCoreV1 "k8s.io/client-go/applyconfigurations/core/v1"
|
||||
applyMetaV1 "k8s.io/client-go/applyconfigurations/meta/v1"
|
||||
applyRbacV1 "k8s.io/client-go/applyconfigurations/rbac/v1"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// TransformApplyObjectToManifest transforms an apply object into a normal Kubernetes manifest
|
||||
func TransformApplyObjectToManifest(applyObject interface{}) string {
|
||||
// Convert the apply object to an unstructured object
|
||||
unstructuredObj := &unstructured.Unstructured{}
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(applyConfigToMap(applyObject), unstructuredObj)
|
||||
if err != nil {
|
||||
log.Fatalf("Error converting apply object to unstructured: %v", err)
|
||||
}
|
||||
|
||||
// Marshal the unstructured object into YAML
|
||||
yamlData, err := yaml.Marshal(unstructuredObj.Object)
|
||||
if err != nil {
|
||||
log.Fatalf("Error marshaling unstructured object to YAML: %v", err)
|
||||
}
|
||||
|
||||
return string(yamlData)
|
||||
}
|
||||
|
||||
// Helper function to convert apply configuration to a map
|
||||
func applyConfigToMap(applyConfig interface{}) map[string]interface{} {
|
||||
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(applyConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Error converting apply configuration to map: %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// GenerateSA will create the service account for kube-vip
|
||||
func GenerateSA() *applyCoreV1.ServiceAccountApplyConfiguration {
|
||||
func GenerateSA(c *Config) *applyCoreV1.ServiceAccountApplyConfiguration {
|
||||
kind := "ServiceAccount"
|
||||
name := "kube-vip"
|
||||
namespace := "kube-system"
|
||||
var namespace string
|
||||
if c.ServiceNamespace != "" {
|
||||
namespace = c.ServiceNamespace
|
||||
} else {
|
||||
namespace = metav1.NamespaceSystem
|
||||
}
|
||||
newManifest := &applyCoreV1.ServiceAccountApplyConfiguration{
|
||||
TypeMetaApplyConfiguration: applyMetaV1.TypeMetaApplyConfiguration{APIVersion: &corev1.SchemeGroupVersion.Version, Kind: &kind},
|
||||
ObjectMetaApplyConfiguration: &applyMetaV1.ObjectMetaApplyConfiguration{
|
||||
@@ -30,15 +65,31 @@ func GenerateSA() *applyCoreV1.ServiceAccountApplyConfiguration {
|
||||
}
|
||||
|
||||
// GenerateCR will generate the Cluster role for kube-vip
|
||||
func GenerateCR() *applyRbacV1.ClusterRoleApplyConfiguration {
|
||||
name := "system:kube-vip-role"
|
||||
roleRefKind := "ClusterRole"
|
||||
apiVersion := "rbac.authorization.k8s.io/v1"
|
||||
func GenerateRole(c *Config, role bool) *applyRbacV1.RoleApplyConfiguration {
|
||||
var kind, name string
|
||||
var namespace *string
|
||||
if role {
|
||||
kind = "Role"
|
||||
name = "kube-vip"
|
||||
if c.ServiceNamespace != "" {
|
||||
namespace = &c.ServiceNamespace
|
||||
} else {
|
||||
// If the namespace is empty then we need to set it to the system namespace
|
||||
copiedNamespace := metav1.NamespaceSystem
|
||||
namespace = &copiedNamespace
|
||||
}
|
||||
|
||||
newManifest := &applyRbacV1.ClusterRoleApplyConfiguration{
|
||||
TypeMetaApplyConfiguration: applyMetaV1.TypeMetaApplyConfiguration{APIVersion: &apiVersion, Kind: &roleRefKind},
|
||||
} else {
|
||||
kind = "ClusterRole"
|
||||
name = "system:kube-vip-role"
|
||||
|
||||
}
|
||||
apiVersion := "rbac.authorization.k8s.io/v1"
|
||||
newManifest := &applyRbacV1.RoleApplyConfiguration{
|
||||
TypeMetaApplyConfiguration: applyMetaV1.TypeMetaApplyConfiguration{APIVersion: &apiVersion, Kind: &kind},
|
||||
ObjectMetaApplyConfiguration: &applyMetaV1.ObjectMetaApplyConfiguration{
|
||||
Name: &name,
|
||||
Name: &name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Rules: []applyRbacV1.PolicyRuleApplyConfiguration{
|
||||
{
|
||||
@@ -49,7 +100,7 @@ func GenerateCR() *applyRbacV1.ClusterRoleApplyConfiguration {
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"services", "endpoints"},
|
||||
Verbs: []string{"list", "get", "watch", "endoints"},
|
||||
Verbs: []string{"list", "get", "watch", "update"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
@@ -61,38 +112,54 @@ func GenerateCR() *applyRbacV1.ClusterRoleApplyConfiguration {
|
||||
Resources: []string{"leases"},
|
||||
Verbs: []string{"list", "get", "watch", "update", "create"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"discovery.k8s.io"},
|
||||
Resources: []string{"endpointslices"},
|
||||
Verbs: []string{"list", "get", "watch", "update"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"pods"},
|
||||
Verbs: []string{"list"},
|
||||
},
|
||||
},
|
||||
}
|
||||
return newManifest
|
||||
}
|
||||
|
||||
// GenerateCRB will generate the clusterRoleBinding
|
||||
func GenerateCRB() *applyRbacV1.ClusterRoleBindingApplyConfiguration {
|
||||
kind := "ClusterRoleBinding"
|
||||
// GenerateCRB will generate the clusterRoleBinding or rolebinding
|
||||
func GenerateRoleBinding(rolebinding bool, saCfg *applyCoreV1.ServiceAccountApplyConfiguration, crCfg *applyRbacV1.RoleApplyConfiguration) *applyRbacV1.RoleBindingApplyConfiguration {
|
||||
apiVersion := "rbac.authorization.k8s.io/v1"
|
||||
subjectKind := "ServiceAccount"
|
||||
apiGroup := "rbac.authorization.k8s.io"
|
||||
roleRefKind := "ClusterRole"
|
||||
roleRefName := "system:kube-vip-role"
|
||||
name := "kube-vip"
|
||||
bindName := "system:kube-vip-role-binding"
|
||||
namespace := "kube-system"
|
||||
|
||||
newManifest := &applyRbacV1.ClusterRoleBindingApplyConfiguration{
|
||||
var kind, bindName string
|
||||
var namespace, objectNamespace *string
|
||||
if rolebinding {
|
||||
kind = "RoleBinding"
|
||||
bindName = "kube-vip"
|
||||
namespace = nil
|
||||
objectNamespace = saCfg.Namespace
|
||||
} else {
|
||||
kind = "ClusterRoleBinding"
|
||||
bindName = "system:kube-vip-binding"
|
||||
namespace = saCfg.Namespace
|
||||
objectNamespace = nil
|
||||
}
|
||||
newManifest := &applyRbacV1.RoleBindingApplyConfiguration{
|
||||
TypeMetaApplyConfiguration: applyMetaV1.TypeMetaApplyConfiguration{APIVersion: &apiVersion, Kind: &kind},
|
||||
ObjectMetaApplyConfiguration: &applyMetaV1.ObjectMetaApplyConfiguration{
|
||||
Name: &bindName,
|
||||
Name: &bindName,
|
||||
Namespace: objectNamespace,
|
||||
},
|
||||
RoleRef: &applyRbacV1.RoleRefApplyConfiguration{
|
||||
APIGroup: &apiGroup,
|
||||
Kind: &roleRefKind,
|
||||
Name: &roleRefName,
|
||||
Kind: crCfg.Kind,
|
||||
Name: crCfg.Name,
|
||||
},
|
||||
Subjects: []applyRbacV1.SubjectApplyConfiguration{
|
||||
{
|
||||
Kind: &subjectKind,
|
||||
Name: &name,
|
||||
Namespace: &namespace,
|
||||
Kind: saCfg.Kind,
|
||||
Name: saCfg.Name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -100,7 +167,11 @@ func GenerateCRB() *applyRbacV1.ClusterRoleBindingApplyConfiguration {
|
||||
}
|
||||
|
||||
// generatePodSpec will take a kube-vip config and generate a Pod spec
|
||||
func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod {
|
||||
func generatePodSpec(c *Config, image, imageVersion string, inCluster bool) *corev1.Pod {
|
||||
imageRef, err := name.NewTag(image, name.WeakValidation, name.WithDefaultTag(imageVersion))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Cannot parse %q: %w", image, err))
|
||||
}
|
||||
command := "manager"
|
||||
|
||||
// Determine where the pods should be living (for multi-tenancy)
|
||||
@@ -121,6 +192,14 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
Name: port,
|
||||
Value: fmt.Sprintf("%d", c.Port),
|
||||
},
|
||||
{
|
||||
Name: nodeName,
|
||||
ValueFrom: &corev1.EnvVarSource{
|
||||
FieldRef: &corev1.ObjectFieldSelector{
|
||||
FieldPath: "spec.nodeName",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// If we're specifically saying which interface to use then add it to the manifest
|
||||
@@ -131,6 +210,13 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
Value: c.Interface,
|
||||
},
|
||||
}
|
||||
// specify if global scope should be set when using the lo interface
|
||||
if c.LoInterfaceGlobalScope {
|
||||
iface = append(iface, corev1.EnvVar{
|
||||
Name: vipInterfaceLoGlobal,
|
||||
Value: strconv.FormatBool(c.LoInterfaceGlobalScope),
|
||||
})
|
||||
}
|
||||
newEnvironment = append(newEnvironment, iface...)
|
||||
}
|
||||
|
||||
@@ -146,18 +232,6 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
newEnvironment = append(newEnvironment, svcInterface...)
|
||||
}
|
||||
|
||||
// If a CIDR is used add it to the manifest
|
||||
if c.VIPCIDR != "" {
|
||||
// build environment variables
|
||||
cidr := []corev1.EnvVar{
|
||||
{
|
||||
Name: vipCidr,
|
||||
Value: c.VIPCIDR,
|
||||
},
|
||||
}
|
||||
newEnvironment = append(newEnvironment, cidr...)
|
||||
}
|
||||
|
||||
// If a subnet is required for the VIP
|
||||
if c.VIPSubnet != "" {
|
||||
// build environment variables
|
||||
@@ -316,40 +390,6 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
|
||||
}
|
||||
|
||||
// If we're specifying a configuration
|
||||
if c.ProviderConfig != "" {
|
||||
provider := []corev1.EnvVar{
|
||||
{
|
||||
Name: providerConfig,
|
||||
Value: c.ProviderConfig,
|
||||
},
|
||||
}
|
||||
newEnvironment = append(newEnvironment, provider...)
|
||||
}
|
||||
|
||||
// If Equinix Metal is enabled then add it to the manifest
|
||||
if c.EnableMetal {
|
||||
packet := []corev1.EnvVar{
|
||||
{
|
||||
Name: vipPacket,
|
||||
Value: strconv.FormatBool(c.EnableMetal),
|
||||
},
|
||||
{
|
||||
Name: vipPacketProject,
|
||||
Value: c.MetalProject,
|
||||
},
|
||||
{
|
||||
Name: vipPacketProjectID,
|
||||
Value: c.MetalProjectID,
|
||||
},
|
||||
{
|
||||
Name: "PACKET_AUTH_TOKEN",
|
||||
Value: c.MetalAPIKey,
|
||||
},
|
||||
}
|
||||
newEnvironment = append(newEnvironment, packet...)
|
||||
}
|
||||
|
||||
// Detect and enable wireguard mode
|
||||
if c.EnableWireguard {
|
||||
wireguard := []corev1.EnvVar{
|
||||
@@ -371,8 +411,7 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
}
|
||||
newEnvironment = append(newEnvironment, routingtable...)
|
||||
}
|
||||
|
||||
// If BGP, but we're not using Equinix Metal
|
||||
// If BGP
|
||||
if c.EnableBGP {
|
||||
bgp := []corev1.EnvVar{
|
||||
{
|
||||
@@ -382,8 +421,9 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
}
|
||||
newEnvironment = append(newEnvironment, bgp...)
|
||||
}
|
||||
// If BGP, but we're not using Equinix Metal
|
||||
if c.EnableBGP && !c.EnableMetal {
|
||||
|
||||
// If BGP
|
||||
if c.EnableBGP {
|
||||
bgpConfig := []corev1.EnvVar{
|
||||
{
|
||||
Name: bgpRouterID,
|
||||
@@ -477,20 +517,18 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
})
|
||||
}
|
||||
|
||||
if c.PrometheusHTTPServer != "" {
|
||||
prometheus := []corev1.EnvVar{
|
||||
{
|
||||
Name: prometheusServer,
|
||||
Value: c.PrometheusHTTPServer,
|
||||
},
|
||||
}
|
||||
newEnvironment = append(newEnvironment, prometheus...)
|
||||
prometheus := []corev1.EnvVar{
|
||||
{
|
||||
Name: prometheusServer,
|
||||
Value: c.PrometheusHTTPServer,
|
||||
},
|
||||
}
|
||||
newEnvironment = append(newEnvironment, prometheus...)
|
||||
|
||||
if c.EnableEndpointSlices {
|
||||
if c.EnableEndpoints {
|
||||
newEnvironment = append(newEnvironment, corev1.EnvVar{
|
||||
Name: enableEndpointSlices,
|
||||
Value: strconv.FormatBool(c.EnableEndpointSlices),
|
||||
Name: enableEndpoints,
|
||||
Value: strconv.FormatBool(c.EnableEndpoints),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -505,6 +543,46 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
newEnvironment = append(newEnvironment, disServiceUpdates...)
|
||||
}
|
||||
|
||||
if c.MirrorDestInterface != "" {
|
||||
mdif := []corev1.EnvVar{
|
||||
{
|
||||
Name: mirrorDestInterface,
|
||||
Value: c.MirrorDestInterface,
|
||||
},
|
||||
}
|
||||
newEnvironment = append(newEnvironment, mdif...)
|
||||
}
|
||||
|
||||
if c.HealthCheckPort != 0 {
|
||||
healthPort := []corev1.EnvVar{
|
||||
{
|
||||
Name: healthCheckPort,
|
||||
Value: fmt.Sprintf("%d", c.HealthCheckPort),
|
||||
},
|
||||
}
|
||||
newEnvironment = append(newEnvironment, healthPort...)
|
||||
}
|
||||
|
||||
var securityContext *corev1.SecurityContext
|
||||
if c.LoadBalancerForwardingMethod == "masquerade" {
|
||||
var privileged = true
|
||||
securityContext = &corev1.SecurityContext{
|
||||
Privileged: &privileged,
|
||||
}
|
||||
} else {
|
||||
securityContext = &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{
|
||||
"NET_ADMIN",
|
||||
"NET_RAW",
|
||||
},
|
||||
Drop: []corev1.Capability{
|
||||
"ALL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
newManifest := &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Pod",
|
||||
@@ -518,16 +596,9 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "kube-vip",
|
||||
Image: fmt.Sprintf("ghcr.io/kube-vip/kube-vip:%s", imageVersion),
|
||||
ImagePullPolicy: corev1.PullAlways,
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
Add: []corev1.Capability{
|
||||
"NET_ADMIN",
|
||||
"NET_RAW",
|
||||
},
|
||||
},
|
||||
},
|
||||
Image: imageRef.Name(),
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
SecurityContext: securityContext,
|
||||
Args: []string{
|
||||
command,
|
||||
},
|
||||
@@ -566,38 +637,18 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
newManifest.Spec.HostAliases = append(newManifest.Spec.HostAliases, hostAlias)
|
||||
}
|
||||
|
||||
if c.ProviderConfig != "" {
|
||||
providerConfigMount := corev1.VolumeMount{
|
||||
Name: "cloud-sa-volume",
|
||||
MountPath: "/etc/cloud-sa",
|
||||
ReadOnly: true,
|
||||
}
|
||||
newManifest.Spec.Containers[0].VolumeMounts = append(newManifest.Spec.Containers[0].VolumeMounts, providerConfigMount)
|
||||
|
||||
providerConfigVolume := corev1.Volume{
|
||||
Name: "cloud-sa-volume",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: "metal-cloud-config",
|
||||
},
|
||||
},
|
||||
}
|
||||
newManifest.Spec.Volumes = append(newManifest.Spec.Volumes, providerConfigVolume)
|
||||
|
||||
}
|
||||
|
||||
return newManifest
|
||||
}
|
||||
|
||||
// GeneratePodManifestFromConfig will take a kube-vip config and generate a manifest
|
||||
func GeneratePodManifestFromConfig(c *Config, imageVersion string, inCluster bool) string {
|
||||
newManifest := generatePodSpec(c, imageVersion, inCluster)
|
||||
func GeneratePodManifestFromConfig(c *Config, image, imageVersion string, inCluster bool) string {
|
||||
newManifest := generatePodSpec(c, image, imageVersion, inCluster)
|
||||
b, _ := yaml.Marshal(newManifest)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// GenerateDaemonsetManifestFromConfig will take a kube-vip config and generate a manifest
|
||||
func GenerateDaemonsetManifestFromConfig(c *Config, imageVersion string, inCluster, taint bool) string {
|
||||
func GenerateDaemonsetManifestFromConfig(c *Config, image, imageVersion string, inCluster, taint bool) string {
|
||||
// Determine where the pod should be deployed
|
||||
var namespace string
|
||||
if c.ServiceNamespace != "" {
|
||||
@@ -606,7 +657,7 @@ func GenerateDaemonsetManifestFromConfig(c *Config, imageVersion string, inClust
|
||||
namespace = metav1.NamespaceSystem
|
||||
}
|
||||
|
||||
podSpec := generatePodSpec(c, imageVersion, inCluster).Spec
|
||||
podSpec := generatePodSpec(c, image, imageVersion, inCluster).Spec
|
||||
newManifest := &appv1.DaemonSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "DaemonSet",
|
||||
@@ -673,6 +724,15 @@ func GenerateDaemonsetManifestFromConfig(c *Config, imageVersion string, inClust
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we don't check error return values for any of these marshall/unmarshall functions
|
||||
b, _ := yaml.Marshal(newManifest)
|
||||
|
||||
// This additional step is required to be able to delete a section of the manifest being generated
|
||||
m := make(map[string]interface{})
|
||||
_ = yaml.Unmarshal(b, &m)
|
||||
delete(m, "status")
|
||||
|
||||
b, _ = yaml.Marshal(m)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package kubevip
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEnvironment(t *testing.T) {
|
||||
|
||||
@@ -9,8 +12,8 @@ func TestParseEnvironment(t *testing.T) {
|
||||
c *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{"", nil, false},
|
||||
{"", &Config{Interface: "eth0", ServicesInterface: "eth1"}, false},
|
||||
{"nil config", nil, false},
|
||||
{"basic config", &Config{Interface: "eth0", ServicesInterface: "eth1"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Logf("%v", tt.c)
|
||||
@@ -21,3 +24,53 @@ func TestParseEnvironment(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvironmentConfigFile(t *testing.T) {
|
||||
// Save original environment
|
||||
originalConfigFile := os.Getenv("config_file")
|
||||
defer func() {
|
||||
if originalConfigFile != "" {
|
||||
os.Setenv("config_file", originalConfigFile)
|
||||
} else {
|
||||
os.Unsetenv("config_file")
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
envValue string
|
||||
expectedConfig string
|
||||
}{
|
||||
{
|
||||
name: "config_file environment variable set",
|
||||
envValue: "/etc/kube-vip/config.yaml",
|
||||
expectedConfig: "/etc/kube-vip/config.yaml",
|
||||
},
|
||||
{
|
||||
name: "config_file environment variable empty",
|
||||
envValue: "",
|
||||
expectedConfig: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Set environment variable
|
||||
if tt.envValue != "" {
|
||||
os.Setenv("config_file", tt.envValue)
|
||||
} else {
|
||||
os.Unsetenv("config_file")
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
err := ParseEnvironment(config)
|
||||
if err != nil {
|
||||
t.Errorf("ParseEnvironment() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if config.ConfigFile != tt.expectedConfig {
|
||||
t.Errorf("ConfigFile = %v, expected %v", config.ConfigFile, tt.expectedConfig)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ package kubevip
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
Auto = "auto"
|
||||
)
|
||||
|
||||
func (c *Config) CheckInterface() error {
|
||||
if c.Interface != "" {
|
||||
if err := isValidInterface(c.Interface); err != nil {
|
||||
@@ -24,6 +29,10 @@ func (c *Config) CheckInterface() error {
|
||||
}
|
||||
|
||||
func isValidInterface(iface string) error {
|
||||
// auto interface discovery for services is enabled
|
||||
if iface == Auto {
|
||||
return nil
|
||||
}
|
||||
l, err := netlink.LinkByName(iface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get %s failed, error: %w", iface, err)
|
||||
@@ -36,8 +45,8 @@ func isValidInterface(iface string) error {
|
||||
// userspace has set operational state. Interface must be considered for user
|
||||
// data as setting operational state has not been implemented in every driver."
|
||||
if attrs.OperState == netlink.OperUnknown {
|
||||
log.Warningf(
|
||||
"the status of the interface %s is unknown. Ensure your interface is ready to accept traffic, if so you can safely ignore this message",
|
||||
log.Warn(
|
||||
"the status of the interface is unknown. Ensure your interface is ready to accept traffic, if so you can safely ignore this message", "interface",
|
||||
iface,
|
||||
)
|
||||
} else if attrs.OperState != netlink.OperUp {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package kubevip
|
||||
|
||||
import (
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
)
|
||||
|
||||
// Config defines all of the settings for the Kube-Vip Pod
|
||||
type Config struct {
|
||||
// Logging, settings
|
||||
Logging int `yaml:"logging"`
|
||||
Logging int32 `yaml:"logging"`
|
||||
|
||||
// EnableARP, will use ARP to advertise the VIP address
|
||||
EnableARP bool `yaml:"enableARP"`
|
||||
@@ -27,6 +23,9 @@ type Config struct {
|
||||
// DetectControlPlane, will attempt to find the control plane from loopback (127.0.0.1)
|
||||
DetectControlPlane bool `yaml:"detectControlPlane"`
|
||||
|
||||
// KubernetesAddr,is the address of the Kubernetes API server on this machine
|
||||
KubernetesAddr string `yaml:"kubernetesAddr"`
|
||||
|
||||
// EnableServices, will enable the services functionality (used for hybrid behaviour)
|
||||
EnableServices bool `yaml:"enableServices"`
|
||||
|
||||
@@ -42,12 +41,19 @@ type Config struct {
|
||||
// LoadBalancerClassName, will limit the load balancing services to services with LoadBalancerClass set to this value
|
||||
LoadBalancerClassName string `yaml:"lbClassName"`
|
||||
|
||||
// LoadBalancerClassLegacyHandling, will enable legacy loadbalancer class handling which does not force service loadbalancer class and kube-vip's loadbalancer class to be the same.
|
||||
LoadBalancerClassLegacyHandling bool `yaml:"lbClassNameLegacyHandling"`
|
||||
|
||||
// EnableServiceSecurity, will enable the use of iptables to secure services
|
||||
EnableServiceSecurity bool `yaml:"EnableServiceSecurity"`
|
||||
|
||||
// ArpBroadcastRate, defines how often kube-vip will update the network about updates to the network
|
||||
ArpBroadcastRate int64 `yaml:"arpBroadcastRate"`
|
||||
|
||||
// PreserveVIPOnLeadershipLoss, if true, VIP addresses will remain on interface when leadership is lost (only ARP/NDP broadcasting stops)
|
||||
// If false, VIP addresses are deleted on leadership loss (legacy behavior)
|
||||
PreserveVIPOnLeadershipLoss bool `yaml:"preserveVipOnLeadershipLoss"`
|
||||
|
||||
// Annotations will define if we're going to wait and lookup configuration from Kubernetes node annotations
|
||||
Annotations string
|
||||
|
||||
@@ -70,14 +76,11 @@ type Config struct {
|
||||
// VipSubnet is the Subnet that is applied to the VIP
|
||||
VIPSubnet string `yaml:"vipSubnet"`
|
||||
|
||||
// VIPCIDR is cidr range for the VIP (primarily needed for BGP)
|
||||
VIPCIDR string `yaml:"vipCidr"`
|
||||
|
||||
// Address is the IP or DNS Name to use as a VirtualIP
|
||||
Address string `yaml:"address"`
|
||||
|
||||
// Listen port for the VirtualIP
|
||||
Port int `yaml:"port"`
|
||||
Port uint16 `yaml:"port"`
|
||||
|
||||
// Namespace will define which namespace the control plane pods will run in
|
||||
Namespace string `yaml:"namespace"`
|
||||
@@ -88,6 +91,9 @@ type Config struct {
|
||||
// use DDNS to allocate IP when Address is set to a DNS Name
|
||||
DDNS bool `yaml:"ddns"`
|
||||
|
||||
// NodeName - used for matching node name from pod spec
|
||||
NodeName string `yaml:"leaseNodeName"`
|
||||
|
||||
// SingleNode will start the cluster as a single Node (Raft disabled)
|
||||
SingleNode bool `yaml:"singleNode"`
|
||||
|
||||
@@ -104,7 +110,7 @@ type Config struct {
|
||||
EnableLoadBalancer bool `yaml:"enableLoadBalancer"`
|
||||
|
||||
// Listen port for the IPVS Service
|
||||
LoadBalancerPort int `yaml:"lbPort"`
|
||||
LoadBalancerPort uint16 `yaml:"lbPort"`
|
||||
|
||||
// Forwarding method for the IPVS Service
|
||||
LoadBalancerForwardingMethod string `yaml:"lbForwardingMethod"`
|
||||
@@ -122,25 +128,10 @@ type Config struct {
|
||||
CleanRoutingTable bool `yaml:"cleanRoutingTable"`
|
||||
|
||||
// BGP Configuration
|
||||
BGPConfig bgp.Config
|
||||
BGPPeerConfig bgp.Peer
|
||||
BGPConfig BGPConfig
|
||||
BGPPeerConfig BGPPeer
|
||||
BGPPeers []string
|
||||
|
||||
// EnableMetal, will use the metal API to update the EIP <-> VIP (if BGP is enabled then BGP will be used)
|
||||
EnableMetal bool `yaml:"enableMetal"`
|
||||
|
||||
// MetalAPIKey, is the API token used to authenticate to the API
|
||||
MetalAPIKey string
|
||||
|
||||
// MetalProject, is the name of a particular defined project
|
||||
MetalProject string
|
||||
|
||||
// MetalProjectID, is the name of a particular defined project
|
||||
MetalProjectID string
|
||||
|
||||
// ProviderConfig, is the path to a provider configuration file
|
||||
ProviderConfig string
|
||||
|
||||
// LoadBalancers are the various services we can load balance over
|
||||
LoadBalancers []LoadBalancer `yaml:"loadBalancers,omitempty"`
|
||||
|
||||
@@ -170,8 +161,34 @@ type Config struct {
|
||||
// DisableServiceUpdates, if true, kube-vip will only advertise service, but it will not update service's Status.LoadBalancer.Ingress slice
|
||||
DisableServiceUpdates bool `yaml:"disableServiceUpdates"`
|
||||
|
||||
// EnableEndpointSlices, if enabled, EndpointSlices will be used instead of Endpoints
|
||||
EnableEndpointSlices bool `yaml:"enableEndpointSlices"`
|
||||
// EnableEndpoints, if enabled, Endpoints will be used instead of EndpointSlices
|
||||
EnableEndpoints bool `yaml:"enableEndpoints"`
|
||||
|
||||
// MirrorDestInterface is the network interface where all traffics that go through service interface
|
||||
// will be mirrored to. If ServicesInterface is not set, fall back to Interface.
|
||||
// + optional
|
||||
MirrorDestInterface string `yaml:"mirrorDestInterface"`
|
||||
|
||||
// IptablesBackend iptables backend, can be specified as `nft` or `legacy`. If not set, it defaults to automatic detection.
|
||||
IptablesBackend string `yaml:"iptablesBackend"`
|
||||
|
||||
// BackendHealthCheckInterval Interval in seconds for checking backend health.
|
||||
BackendHealthCheckInterval int `yaml:"backendHealthCheckInterval"`
|
||||
|
||||
// LoInterfaceGlobalScope, if true will set global scope when using the lo interface, otherwise a host scope will be used
|
||||
LoInterfaceGlobalScope bool `yaml:"loInterfaceGlobalScope"`
|
||||
|
||||
// HealthCheckPort, if non-zero then will enable the healthcheck to return ok on this port
|
||||
HealthCheckPort int `yaml:"healthCheckPort"`
|
||||
|
||||
// EnableUPNP, enables UPNP functions
|
||||
EnableUPNP bool `yaml:"enableUPNP"`
|
||||
|
||||
// EgressClean, enables egress cleaning on Kube-vip's start
|
||||
EgressClean bool `yaml:"egressClean"`
|
||||
|
||||
// ConfigFile defines the path to a JSON/YAML configuration file
|
||||
ConfigFile string `yaml:"configFile"`
|
||||
}
|
||||
|
||||
// KubernetesLeaderElection defines all of the settings for Kubernetes KubernetesLeaderElection
|
||||
@@ -188,7 +205,7 @@ type KubernetesLeaderElection struct {
|
||||
// RenewDeadline - length of time a host can attempt to renew its lease
|
||||
RenewDeadline int
|
||||
|
||||
// RetryPerion - Number of times the host will retry to hold a lease
|
||||
// RetryPeriod - length of time (in seconds) the LeaderElector clients should wait between tries of actions
|
||||
RetryPeriod int
|
||||
|
||||
// LeaseAnnotations - annotations which will be given to the lease object
|
||||
@@ -208,11 +225,8 @@ type LoadBalancer struct {
|
||||
// Name of a LoadBalancer
|
||||
Name string `yaml:"name"`
|
||||
|
||||
// Type of LoadBalancer, either TCP of HTTP(s)
|
||||
Type string `yaml:"type"`
|
||||
|
||||
// Listening frontend port of this LoadBalancer instance
|
||||
Port int `yaml:"port"`
|
||||
//Ports exposed by a LoadBalancer
|
||||
Ports []Port
|
||||
|
||||
// BindToVip will bind the load balancer port to the VIP itself
|
||||
BindToVip bool `yaml:"bindToVip"`
|
||||
@@ -220,3 +234,11 @@ type LoadBalancer struct {
|
||||
// Forwarding method of LoadBalancer, either Local, Tunnel, DirectRoute or Bypass
|
||||
ForwardingMethod string `yaml:"forwardingMethod"`
|
||||
}
|
||||
|
||||
type Port struct {
|
||||
// Type of LoadBalancer, either TCP or UDP
|
||||
Type string `yaml:"type"`
|
||||
|
||||
// Listening frontend port of this LoadBalancer instance
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
5
pkg/kubevip/constants.go
Normal file
5
pkg/kubevip/constants.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package kubevip
|
||||
|
||||
const (
|
||||
LBClassName = "kube-vip.io/kube-vip-class"
|
||||
)
|
||||
7
pkg/kubevip/labels.go
Normal file
7
pkg/kubevip/labels.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package kubevip
|
||||
|
||||
const (
|
||||
// label used on nodes, which announce the LoadBalancer IP
|
||||
HasIP = "kube-vip.io/has-ip"
|
||||
HasIPJSONPath = `kube-vip.io~1has-ip`
|
||||
)
|
||||
127
pkg/lease/lease.go
Normal file
127
pkg/lease/lease.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package lease
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// Manager is used to manage leases.
|
||||
type Manager struct {
|
||||
leases map[string]*Lease
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewManager creates new lease manager.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
leases: make(map[string]*Lease),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds lease or incerements counter if lease is alreay used.
|
||||
func (m *Manager) Add(service *v1.Service) (*Lease, bool) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
if _, exist := m.leases[id]; !exist {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.leases[id] = newLease(ctx, cancel)
|
||||
return m.leases[id], true
|
||||
}
|
||||
|
||||
m.leases[id].increment()
|
||||
return m.leases[id], false
|
||||
}
|
||||
|
||||
// Delete decrements lease counter and removes the lease if counter equals 0.
|
||||
func (m *Manager) Delete(service *v1.Service) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
if _, exist := m.leases[id]; exist {
|
||||
m.leases[id].decrement()
|
||||
if m.leases[id].cnt < 1 {
|
||||
delete(m.leases, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns lease for the service.
|
||||
func (m *Manager) Get(service *v1.Service) *Lease {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
|
||||
if lease, exist := m.leases[id]; exist {
|
||||
return lease
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLeaderContext returns leder context for the service.
|
||||
func (m *Manager) GetLeaderContext(service *v1.Service) context.Context {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
if _, ok := m.leases[id]; !ok {
|
||||
return nil
|
||||
}
|
||||
return m.leases[id].Ctx
|
||||
}
|
||||
|
||||
// Lease holds lease data.
|
||||
type Lease struct {
|
||||
cnt uint
|
||||
Lock *sync.Mutex
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
Started chan any
|
||||
}
|
||||
|
||||
func newLease(ctx context.Context, cancel context.CancelFunc) *Lease {
|
||||
return &Lease{
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
cnt: 1,
|
||||
Lock: new(sync.Mutex),
|
||||
Started: make(chan any),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lease) increment() {
|
||||
l.Lock.Lock()
|
||||
defer l.Lock.Unlock()
|
||||
l.cnt++
|
||||
}
|
||||
|
||||
func (l *Lease) decrement() {
|
||||
l.Lock.Lock()
|
||||
defer l.Lock.Unlock()
|
||||
if l.cnt == 0 {
|
||||
return
|
||||
}
|
||||
l.cnt--
|
||||
if l.cnt < 1 {
|
||||
l.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// GetName gets lease name and id for the service.
|
||||
func GetName(service *v1.Service) (string, string) {
|
||||
serviceLease, exists := service.Annotations[kubevip.ServiceLease]
|
||||
if !exists || serviceLease == "" {
|
||||
serviceLease = fmt.Sprintf("kubevip-%s", service.Name)
|
||||
}
|
||||
serviceLeaseID := fmt.Sprintf("%s/%s", serviceLease, service.Namespace)
|
||||
return serviceLease, serviceLeaseID
|
||||
}
|
||||
|
||||
// UsesCommon checks if service uses common lease feature.
|
||||
func UsesCommon(service *v1.Service) bool {
|
||||
_, common := service.Annotations[kubevip.ServiceLease]
|
||||
return common
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
package loadbalancer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/cloudflare/ipvs"
|
||||
"github.com/cloudflare/ipvs/netmask"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/kube-vip/kube-vip/pkg/backend"
|
||||
"github.com/kube-vip/kube-vip/pkg/sysctl"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -36,32 +45,62 @@ const (
|
||||
type IPVSLoadBalancer struct {
|
||||
client ipvs.Client
|
||||
loadBalancerService ipvs.Service
|
||||
Port int
|
||||
Port uint16
|
||||
forwardingMethod ipvs.ForwardType
|
||||
backendMap backend.Map
|
||||
interval int
|
||||
lock sync.Mutex
|
||||
stop chan struct{}
|
||||
networkInterface string
|
||||
leaderCancel context.CancelFunc
|
||||
signal chan os.Signal
|
||||
address string
|
||||
family ipvs.AddressFamily
|
||||
}
|
||||
|
||||
func NewIPVSLB(address string, port int, forwardingMethod string) (*IPVSLoadBalancer, error) {
|
||||
func NewIPVSLB(address string, port uint16, forwardingMethod string, backendHealthCheckInterval int, networkInterface string, leaderCancel context.CancelFunc, signal chan os.Signal) (*IPVSLoadBalancer, error) {
|
||||
log.Info("Starting IPVS LoadBalancer", "address", address)
|
||||
|
||||
// Create IPVS client
|
||||
c, err := ipvs.New()
|
||||
if err != nil {
|
||||
log.Errorf("ensure IPVS kernel modules are loaded")
|
||||
log.Fatalf("Error starting IPVS [%v]", err)
|
||||
log.Error("ensure IPVS kernel modules are loaded")
|
||||
log.Error("Error starting IPVS", "err", err)
|
||||
panic("")
|
||||
}
|
||||
i, err := c.Info()
|
||||
if err != nil {
|
||||
log.Errorf("ensure IPVS kernel modules are loaded")
|
||||
log.Fatalf("Error getting IPVS version [%v]", err)
|
||||
log.Error("ensure IPVS kernel modules are loaded")
|
||||
log.Error("Error retrieving IPVS info", "err", err)
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
log.Error("no permission to get IPVS info - please ensure that kube-vip is running with proper capabilities/privileged mode")
|
||||
}
|
||||
panic("")
|
||||
}
|
||||
log.Infof("IPVS Loadbalancer enabled for %d.%d.%d", i.Version[0], i.Version[1], i.Version[2])
|
||||
log.Info("IPVS Loadbalancer enabled", "version", fmt.Sprintf("%d.%d.%d", i.Version[0], i.Version[1], i.Version[2]))
|
||||
|
||||
ip, family := ipAndFamily(address)
|
||||
|
||||
if strings.ToLower(forwardingMethod) == "masquerade" {
|
||||
enableProcSys("/proc/sys/net/ipv4/vs/conntrack", "net.ipv4.vs.conntrack")
|
||||
if family == ipvs.INET6 {
|
||||
enableProcSys("/proc/sys/net/ipv6/conf/all/forwarding", "net.ipv6.conf.all.forwarding")
|
||||
} else {
|
||||
enableProcSys("/proc/sys/net/ipv4/ip_forward", "net.ipv4.ip_forward")
|
||||
}
|
||||
}
|
||||
|
||||
netMask := netmask.MaskFrom(31, 32) // For ipv4
|
||||
if family == ipvs.INET6 {
|
||||
netMask = netmask.MaskFrom(128, 128) // For ipv6
|
||||
}
|
||||
|
||||
// Generate out API Server LoadBalancer instance
|
||||
svc := ipvs.Service{
|
||||
Netmask: netmask.MaskFrom(31, 32),
|
||||
Netmask: netMask,
|
||||
Family: family,
|
||||
Protocol: ipvs.TCP,
|
||||
Port: uint16(port),
|
||||
Port: port,
|
||||
Address: ip,
|
||||
Scheduler: ROUNDROBIN,
|
||||
}
|
||||
@@ -80,7 +119,11 @@ func NewIPVSLB(address string, port int, forwardingMethod string) (*IPVSLoadBala
|
||||
m = ipvs.Bypass
|
||||
default:
|
||||
m = ipvs.Local
|
||||
log.Warnf("unknown forwarding method. Defaulting to Local")
|
||||
log.Warn("unknown forwarding method. Defaulting to Local")
|
||||
}
|
||||
|
||||
if backendHealthCheckInterval <= 0 {
|
||||
backendHealthCheckInterval = 5
|
||||
}
|
||||
|
||||
lb := &IPVSLoadBalancer{
|
||||
@@ -88,12 +131,36 @@ func NewIPVSLB(address string, port int, forwardingMethod string) (*IPVSLoadBala
|
||||
client: c,
|
||||
loadBalancerService: svc,
|
||||
forwardingMethod: m,
|
||||
interval: backendHealthCheckInterval,
|
||||
backendMap: make(backend.Map),
|
||||
stop: make(chan struct{}),
|
||||
networkInterface: networkInterface,
|
||||
leaderCancel: leaderCancel,
|
||||
signal: signal,
|
||||
address: address,
|
||||
family: family,
|
||||
}
|
||||
|
||||
go lb.healthCheck()
|
||||
|
||||
// Return our created load-balancer
|
||||
return lb, nil
|
||||
}
|
||||
|
||||
func enableProcSys(path, name string) {
|
||||
isSet, err := sysctl.EnableProcSys(path)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("ensuring %s enabled", name), "err", err)
|
||||
panic("")
|
||||
}
|
||||
if isSet {
|
||||
log.Info(fmt.Sprintf("sysctl set %s to 1", name))
|
||||
}
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) RemoveIPVSLB() error {
|
||||
log.Info("Stopping IPVS LoadBalancer", "address", lb.address)
|
||||
close(lb.stop)
|
||||
err := lb.client.RemoveService(lb.loadBalancerService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error removing existing IPVS service: %v", err)
|
||||
@@ -101,20 +168,54 @@ func (lb *IPVSLoadBalancer) RemoveIPVSLB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) AddBackend(address string, port int) error {
|
||||
func (lb *IPVSLoadBalancer) AddBackend(address string, port uint16) error {
|
||||
isLocal := false
|
||||
var err error
|
||||
|
||||
// Discard backend if it is of different IP family than LB address.
|
||||
if _, family := ipAndFamily(address); family != lb.family {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lb.forwardingMethod == ipvs.Local {
|
||||
log.Info("checking if backend is local", "addr", address)
|
||||
isLocal, err = lb.isLocal(address)
|
||||
if err != nil {
|
||||
log.Error("checking if backend is local", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
backend := backend.Entry{Addr: address, Port: port, IsLocal: isLocal}
|
||||
|
||||
lb.lock.Lock()
|
||||
defer lb.lock.Unlock()
|
||||
if _, ok := lb.backendMap[backend]; !ok {
|
||||
isHealth := backend.Check()
|
||||
if isHealth {
|
||||
err := lb.addBackend(address, port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
lb.backendMap[backend] = isHealth
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) addBackend(address string, port uint16) error {
|
||||
backend := backend.Entry{Addr: address, Port: port}
|
||||
// Check if this is the first backend
|
||||
backends, err := lb.client.Destinations(lb.loadBalancerService)
|
||||
if err != nil && strings.Contains(err.Error(), "file does not exist") {
|
||||
log.Errorf("Error querying backends %s", err)
|
||||
log.Error("querying backends", "err", err)
|
||||
}
|
||||
// If this is our first backend, then we can create the load-balancer service and add a backend
|
||||
if len(backends) == 0 {
|
||||
err = lb.client.CreateService(lb.loadBalancerService)
|
||||
// If we've an error it could be that the IPVS lb instance has been left from a previous leadership
|
||||
if err != nil && strings.Contains(err.Error(), "file exists") {
|
||||
log.Warnf("load balancer for API server already exists, attempting to remove and re-create")
|
||||
log.Warn("load balancer for API server already exists, attempting to remove and re-create")
|
||||
err = lb.client.RemoveService(lb.loadBalancerService)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error re-creating IPVS service: %v", err)
|
||||
}
|
||||
@@ -124,13 +225,15 @@ func (lb *IPVSLoadBalancer) AddBackend(address string, port int) error {
|
||||
}
|
||||
} else if err != nil {
|
||||
// Fatal error at this point as IPVS is probably not working
|
||||
log.Errorf("Unable to create an IPVS service, ensure IPVS kernel modules are loaded")
|
||||
log.Fatalf("IPVS service error: %v", err)
|
||||
log.Error("Unable to create an IPVS service, ensure IPVS kernel modules are loaded")
|
||||
log.Error("IPVS service", "err", err)
|
||||
panic("")
|
||||
|
||||
}
|
||||
log.Infof("Created Load-Balancer services on [%s:%d]", lb.addrString(), lb.Port)
|
||||
log.Info("load-Balancer services created", "address", lb.addrString(), "port", lb.Port)
|
||||
}
|
||||
|
||||
ip, family := ipAndFamily(address)
|
||||
ip, family := ipAndFamily(backend.Addr)
|
||||
|
||||
// Ignore backends that use a different address family.
|
||||
// Looks like different families could be supported in tunnel mode...
|
||||
@@ -140,7 +243,7 @@ func (lb *IPVSLoadBalancer) AddBackend(address string, port int) error {
|
||||
|
||||
dst := ipvs.Destination{
|
||||
Address: ip,
|
||||
Port: uint16(port),
|
||||
Port: backend.Port,
|
||||
Family: family,
|
||||
Weight: 1,
|
||||
FwdMethod: lb.forwardingMethod,
|
||||
@@ -156,12 +259,29 @@ func (lb *IPVSLoadBalancer) AddBackend(address string, port int) error {
|
||||
// file exists is fine, we will just return at this point
|
||||
return nil
|
||||
}
|
||||
log.Infof("Added backend for [%s:%d] on [%s:%d]", lb.addrString(), lb.Port, address, port)
|
||||
log.Info("backend added", "src addr", lb.addrString(), "src port", lb.Port, "dst addr", backend.Addr, "dst port", backend.Port)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) RemoveBackend(address string, port int) error {
|
||||
func (lb *IPVSLoadBalancer) RemoveBackend(address string, port uint16) error {
|
||||
backend := backend.Entry{Addr: address, Port: port}
|
||||
|
||||
lb.lock.Lock()
|
||||
defer lb.lock.Unlock()
|
||||
|
||||
if _, ok := lb.backendMap[backend]; ok {
|
||||
err := lb.removeBackend(address, port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(lb.backendMap, backend)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) removeBackend(address string, port uint16) error {
|
||||
ip, family := ipAndFamily(address)
|
||||
if family != lb.loadBalancerService.Family {
|
||||
return nil
|
||||
@@ -169,12 +289,12 @@ func (lb *IPVSLoadBalancer) RemoveBackend(address string, port int) error {
|
||||
|
||||
dst := ipvs.Destination{
|
||||
Address: ip,
|
||||
Port: uint16(port),
|
||||
Port: port,
|
||||
Family: family,
|
||||
Weight: 1,
|
||||
}
|
||||
err := lb.client.RemoveDestination(lb.loadBalancerService, dst)
|
||||
if err != nil {
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("error removing backend: %v", err)
|
||||
}
|
||||
return nil
|
||||
@@ -185,10 +305,87 @@ func (lb *IPVSLoadBalancer) addrString() string {
|
||||
}
|
||||
|
||||
func ipAndFamily(address string) (netip.Addr, ipvs.AddressFamily) {
|
||||
|
||||
ipAddr := net.ParseIP(address)
|
||||
if ipAddr.To4() == nil {
|
||||
return netip.AddrFrom16([16]byte(ipAddr.To16())), ipvs.INET6
|
||||
}
|
||||
return netip.AddrFrom4([4]byte(ipAddr.To4())), ipvs.INET
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) healthCheck() {
|
||||
backend.Watch(func() {
|
||||
lb.lock.Lock()
|
||||
defer lb.lock.Unlock()
|
||||
for backend, oldStatus := range lb.backendMap {
|
||||
newStatus := backend.Check()
|
||||
if newStatus {
|
||||
// old status -> health
|
||||
if !oldStatus {
|
||||
err := lb.addBackend(backend.Addr, backend.Port)
|
||||
if err != nil {
|
||||
log.Error("add backend", "err", err)
|
||||
}
|
||||
lb.backendMap[backend] = newStatus
|
||||
}
|
||||
} else {
|
||||
// old status -> not health
|
||||
if oldStatus {
|
||||
log.Info("healthCheck failed - removing backend", "address", backend.Addr, "port", backend.Port)
|
||||
err := lb.removeBackend(backend.Addr, backend.Port)
|
||||
if err != nil {
|
||||
log.Error("failed to remove backend", "address", backend.Addr, "port", backend.Port, "err", err)
|
||||
}
|
||||
lb.backendMap[backend] = newStatus
|
||||
}
|
||||
if lb.forwardingMethod == ipvs.Local && !lb.localBackendExists() {
|
||||
if lb.signal != nil {
|
||||
close(lb.signal)
|
||||
}
|
||||
|
||||
if lb.leaderCancel != nil {
|
||||
lb.leaderCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, lb.interval, lb.stop)
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) isLocal(address string) (bool, error) {
|
||||
link, err := netlink.LinkByName(lb.networkInterface)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("getting link '%s': %w", lb.networkInterface, err)
|
||||
}
|
||||
|
||||
family := netlink.FAMILY_V6
|
||||
if utils.IsIPv4(address) {
|
||||
family = netlink.FAMILY_V4
|
||||
}
|
||||
|
||||
target := net.ParseIP(address)
|
||||
if target == nil {
|
||||
return false, fmt.Errorf("address '%s' is not a valid IP address", address)
|
||||
}
|
||||
|
||||
addrs, err := netlink.AddrList(link, family)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("listing addresses for link '%s': %w", lb.networkInterface, err)
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.Equal(target) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (lb *IPVSLoadBalancer) localBackendExists() bool {
|
||||
for backend, isHealthy := range lb.backendMap {
|
||||
if backend.IsLocal && isHealthy {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/ipvs"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
)
|
||||
|
||||
func Test_ipAndFamily(t *testing.T) {
|
||||
@@ -19,13 +20,21 @@ func Test_ipAndFamily(t *testing.T) {
|
||||
want1 ipvs.AddressFamily
|
||||
}{
|
||||
{
|
||||
name: "IPv4",
|
||||
name: utils.IPv4Family,
|
||||
args: args{
|
||||
address: "192.168.0.20",
|
||||
},
|
||||
want: netip.AddrFrom4([4]byte{192, 168, 0, 20}),
|
||||
want1: ipvs.INET,
|
||||
},
|
||||
{
|
||||
name: utils.IPv6Family,
|
||||
args: args{
|
||||
address: "ff02::3",
|
||||
},
|
||||
want: netip.AddrFrom16([16]byte{0: 0xff, 1: 0x02, 15: 0x03}),
|
||||
want1: ipvs.INET6,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -15,6 +15,7 @@ func initClusterManager(sm *Manager) (*cluster.Manager, error) {
|
||||
switch sm.config.LeaderElectionType {
|
||||
case "kubernetes", "":
|
||||
m.KubernetesClient = sm.clientSet
|
||||
m.RetryWatcherClient = sm.rwClientSet
|
||||
case "etcd":
|
||||
client, err := etcd.NewClient(sm.config)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
// Instance defines an instance of everything needed to manage vips
|
||||
type Instance struct {
|
||||
// Virtual IP / Load Balancer configuration
|
||||
vipConfigs []*kubevip.Config
|
||||
|
||||
// cluster instances
|
||||
clusters []*cluster.Cluster
|
||||
|
||||
// Service uses DHCP
|
||||
isDHCP bool
|
||||
dhcpInterface string
|
||||
dhcpInterfaceHwaddr string
|
||||
dhcpInterfaceIP string
|
||||
dhcpHostname string
|
||||
dhcpClient *vip.DHCPClient
|
||||
|
||||
// Kubernetes service mapping
|
||||
VIPs []string
|
||||
Port int32
|
||||
UID string
|
||||
Type string
|
||||
|
||||
serviceSnapshot *v1.Service
|
||||
}
|
||||
|
||||
func NewInstance(svc *v1.Service, config *kubevip.Config) (*Instance, error) {
|
||||
instanceAddresses := fetchServiceAddresses(svc)
|
||||
instanceUID := string(svc.UID)
|
||||
|
||||
// Detect if we're using a specific interface for services
|
||||
var serviceInterface string
|
||||
if config.ServicesInterface != "" {
|
||||
serviceInterface = config.ServicesInterface
|
||||
} else {
|
||||
serviceInterface = config.Interface
|
||||
}
|
||||
|
||||
var newVips []*kubevip.Config
|
||||
|
||||
for _, address := range instanceAddresses {
|
||||
// Generate new Virtual IP configuration
|
||||
newVips = append(newVips, &kubevip.Config{
|
||||
VIP: address,
|
||||
Interface: serviceInterface,
|
||||
SingleNode: true,
|
||||
EnableARP: config.EnableARP,
|
||||
EnableBGP: config.EnableBGP,
|
||||
VIPCIDR: config.VIPCIDR,
|
||||
VIPSubnet: config.VIPSubnet,
|
||||
EnableRoutingTable: config.EnableRoutingTable,
|
||||
RoutingTableID: config.RoutingTableID,
|
||||
RoutingTableType: config.RoutingTableType,
|
||||
RoutingProtocol: config.RoutingProtocol,
|
||||
ArpBroadcastRate: config.ArpBroadcastRate,
|
||||
EnableServiceSecurity: config.EnableServiceSecurity,
|
||||
DNSMode: config.DNSMode,
|
||||
DisableServiceUpdates: config.DisableServiceUpdates,
|
||||
EnableServicesElection: config.EnableServicesElection,
|
||||
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
|
||||
EnableLeaderElection: config.EnableLeaderElection,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create new service
|
||||
instance := &Instance{
|
||||
UID: instanceUID,
|
||||
VIPs: instanceAddresses,
|
||||
serviceSnapshot: svc,
|
||||
}
|
||||
if len(svc.Spec.Ports) > 0 {
|
||||
instance.Type = string(svc.Spec.Ports[0].Protocol)
|
||||
instance.Port = svc.Spec.Ports[0].Port
|
||||
}
|
||||
|
||||
if svc.Annotations != nil {
|
||||
instance.dhcpInterfaceHwaddr = svc.Annotations[hwAddrKey]
|
||||
instance.dhcpInterfaceIP = svc.Annotations[requestedIP]
|
||||
instance.dhcpHostname = svc.Annotations[loadbalancerHostname]
|
||||
}
|
||||
|
||||
// Generate Load Balancer config
|
||||
newLB := kubevip.LoadBalancer{
|
||||
Name: fmt.Sprintf("%s-load-balancer", svc.Name),
|
||||
Port: int(instance.Port),
|
||||
Type: instance.Type,
|
||||
BindToVip: true,
|
||||
}
|
||||
for _, vip := range newVips {
|
||||
// Add Load Balancer Configuration
|
||||
vip.LoadBalancers = append(vip.LoadBalancers, newLB)
|
||||
}
|
||||
// Create Add configuration to the new service
|
||||
instance.vipConfigs = newVips
|
||||
|
||||
// If this was purposely created with the address 0.0.0.0,
|
||||
// we will create a macvlan on the main interface and a DHCP client
|
||||
// TODO: Consider how best to handle DHCP with multiple addresses
|
||||
if len(instanceAddresses) == 1 && instanceAddresses[0] == "0.0.0.0" {
|
||||
err := instance.startDHCP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case err := <-instance.dhcpClient.ErrorChannel():
|
||||
return nil, fmt.Errorf("error starting DHCP for %s/%s: error: %s",
|
||||
instance.serviceSnapshot.Namespace, instance.serviceSnapshot.Name, err)
|
||||
case ip := <-instance.dhcpClient.IPChannel():
|
||||
instance.vipConfigs[0].VIP = ip
|
||||
instance.dhcpInterfaceIP = ip
|
||||
}
|
||||
}
|
||||
for _, vipConfig := range instance.vipConfigs {
|
||||
c, err := cluster.InitCluster(vipConfig, false)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add Service %s/%s", svc.Namespace, svc.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range c.Network {
|
||||
c.Network[i].SetServicePorts(svc)
|
||||
}
|
||||
|
||||
instance.clusters = append(instance.clusters, c)
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (i *Instance) startDHCP() error {
|
||||
if len(i.vipConfigs) != 1 {
|
||||
return fmt.Errorf("DHCP requires exactly 1 VIP config, got: %v", len(i.vipConfigs))
|
||||
}
|
||||
parent, err := netlink.LinkByName(i.vipConfigs[0].Interface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding VIP Interface, for building DHCP Link : %v", err)
|
||||
}
|
||||
|
||||
// Generate name from UID
|
||||
interfaceName := fmt.Sprintf("vip-%s", i.UID[0:8])
|
||||
|
||||
// Check if the interface doesn't exist first
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
log.Infof("Creating new macvlan interface for DHCP [%s]", interfaceName)
|
||||
|
||||
hwaddr, err := net.ParseMAC(i.dhcpInterfaceHwaddr)
|
||||
if i.dhcpInterfaceHwaddr != "" && err != nil {
|
||||
return err
|
||||
} else if hwaddr == nil {
|
||||
hwaddr, err = net.ParseMAC(vip.GenerateMac())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("New interface [%s] mac is %s", interfaceName, hwaddr)
|
||||
mac := &netlink.Macvlan{
|
||||
LinkAttrs: netlink.LinkAttrs{
|
||||
Name: interfaceName,
|
||||
ParentIndex: parent.Attrs().Index,
|
||||
HardwareAddr: hwaddr,
|
||||
},
|
||||
Mode: netlink.MACVLAN_MODE_DEFAULT,
|
||||
}
|
||||
|
||||
err = netlink.LinkAdd(mac)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add %s: %v", interfaceName, err)
|
||||
}
|
||||
|
||||
err = netlink.LinkSetUp(mac)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not bring up interface [%s] : %v", interfaceName, err)
|
||||
}
|
||||
|
||||
iface, err = net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding new DHCP interface by name [%v]", err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Using existing macvlan interface for DHCP [%s]", interfaceName)
|
||||
}
|
||||
|
||||
var initRebootFlag bool
|
||||
if i.dhcpInterfaceIP != "" {
|
||||
initRebootFlag = true
|
||||
}
|
||||
|
||||
client := vip.NewDHCPClient(iface, initRebootFlag, i.dhcpInterfaceIP)
|
||||
|
||||
// Add hostname to dhcp client if annotated
|
||||
if i.dhcpHostname != "" {
|
||||
log.Infof("Hostname specified for dhcp lease: [%s] - [%s]", interfaceName, i.dhcpHostname)
|
||||
client.WithHostName(i.dhcpHostname)
|
||||
}
|
||||
|
||||
go client.Start()
|
||||
|
||||
// Set that DHCP is enabled
|
||||
i.isDHCP = true
|
||||
// Set the name of the interface so that it can be removed on Service deletion
|
||||
i.dhcpInterface = interfaceName
|
||||
i.dhcpInterfaceHwaddr = iface.HardwareAddr.String()
|
||||
// Add the client so that we can call it to stop function
|
||||
i.dhcpClient = client
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,43 +1,46 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/k8s"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kamhlos/upnp"
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/k8s"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
||||
"github.com/kube-vip/kube-vip/pkg/node"
|
||||
"github.com/kube-vip/kube-vip/pkg/services"
|
||||
"github.com/kube-vip/kube-vip/pkg/upnp"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
const plunderLock = "plndr-svcs-lock"
|
||||
|
||||
// Manager degines the manager of the load-balancing services
|
||||
type Manager struct {
|
||||
clientSet *kubernetes.Clientset
|
||||
configMap string
|
||||
config *kubevip.Config
|
||||
clientSet *kubernetes.Clientset
|
||||
rwClientSet *kubernetes.Clientset
|
||||
configMap string
|
||||
config *kubevip.Config
|
||||
|
||||
// Manager services
|
||||
// service bool
|
||||
|
||||
// Keeps track of all running instances
|
||||
serviceInstances []*Instance
|
||||
|
||||
// Additional functionality
|
||||
upnp *upnp.Upnp
|
||||
|
||||
//BGP Manager, this is a singleton that manages all BGP advertisements
|
||||
// BGP Manager, this is a singleton that manages all BGP advertisements
|
||||
bgpServer *bgp.Server
|
||||
|
||||
// This channel is used to catch an OS signal and trigger a shutdown
|
||||
@@ -46,6 +49,8 @@ type Manager struct {
|
||||
// This channel is used to signal a shutdown
|
||||
shutdownChan chan struct{}
|
||||
|
||||
svcProcessor *services.Processor
|
||||
|
||||
// This is a prometheus counter used to count the number of events received
|
||||
// from the service watcher
|
||||
countServiceWatchEvent *prometheus.CounterVec
|
||||
@@ -56,52 +61,105 @@ type Manager struct {
|
||||
|
||||
// This mutex is to protect calls from various goroutines
|
||||
mutex sync.Mutex
|
||||
|
||||
// This tracks used network interfaces and guards them with mutex for concurrent changes.
|
||||
intfMgr *networkinterface.Manager
|
||||
|
||||
// This tracks VIPs and performs ARP/NDP advertisement.
|
||||
arpMgr *arp.Manager
|
||||
|
||||
// This tracks node labels and performs label management
|
||||
// implementation will be decided in constructor
|
||||
// based on config.EnableNodeLabeling
|
||||
nodeLabelManager node.LabelManager
|
||||
}
|
||||
|
||||
// New will create a new managing object
|
||||
func New(configMap string, config *kubevip.Config) (*Manager, error) {
|
||||
|
||||
var clientset *kubernetes.Clientset
|
||||
var err error
|
||||
// Instance identity should be the same as k8s node name to ensure better compatibility.
|
||||
// By default k8s sets node name to `hostname -s`,
|
||||
// so if node name is not provided in the config,
|
||||
// we set it to hostname as a fallback.
|
||||
// This mimics legacy behavior and should work on old kube-vip installations.
|
||||
if config.NodeName == "" {
|
||||
log.Warn("Node name is missing from the config, fall back to hostname")
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get hostname: %v", err)
|
||||
}
|
||||
config.NodeName = hostname
|
||||
}
|
||||
log.Info("using node name", "name", config.NodeName)
|
||||
|
||||
adminConfigPath := "/etc/kubernetes/admin.conf"
|
||||
homeConfigPath := filepath.Join(os.Getenv("HOME"), ".kube", "config")
|
||||
|
||||
var clientset *kubernetes.Clientset
|
||||
var clientConfig *rest.Config
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case config.LeaderElectionType == "etcd":
|
||||
// Do nothing, we don't construct a k8s client for etcd leader election
|
||||
case fileExists(adminConfigPath):
|
||||
if config.EnableControlPlane {
|
||||
case utils.FileExists(adminConfigPath):
|
||||
if config.KubernetesAddr != "" {
|
||||
log.Info("k8s address", "address", config.KubernetesAddr)
|
||||
clientConfig, err = k8s.NewRestConfig(adminConfigPath, false, config.KubernetesAddr)
|
||||
} else if config.EnableControlPlane {
|
||||
// If this is a control plane host it will likely have started as a static pod or won't have the
|
||||
// VIP up before trying to connect to the API server, we set the API endpoint to this machine to
|
||||
// ensure connectivity.
|
||||
if config.DetectControlPlane {
|
||||
clientset, err = k8s.FindWorkingKubernetesAddress(adminConfigPath, false)
|
||||
clientConfig, err = k8s.FindWorkingKubernetesAddress(adminConfigPath, false)
|
||||
} else {
|
||||
// This will attempt to use kubernetes as the hostname (this should be passed as a host alias) in the pod manifest
|
||||
clientset, err = k8s.NewClientset(adminConfigPath, false, fmt.Sprintf("kubernetes:%v", config.Port))
|
||||
clientConfig, err = k8s.NewRestConfig(adminConfigPath, false, fmt.Sprintf("kubernetes:%v", config.Port))
|
||||
}
|
||||
} else {
|
||||
clientset, err = k8s.NewClientset(adminConfigPath, false, "")
|
||||
clientConfig, err = k8s.NewRestConfig(adminConfigPath, false, "")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create k8s clientset from external file: %q: %v", adminConfigPath, err)
|
||||
return nil, fmt.Errorf("could not create k8s REST config from external file: %q: %w", adminConfigPath, err)
|
||||
}
|
||||
log.Debugf("Using external Kubernetes configuration from file [%s]", adminConfigPath)
|
||||
case fileExists(homeConfigPath):
|
||||
clientset, err = k8s.NewClientset(homeConfigPath, false, "")
|
||||
if clientset, err = k8s.NewClientset(clientConfig); err != nil {
|
||||
return nil, fmt.Errorf("could not create k8s clientset: %w", err)
|
||||
}
|
||||
log.Debug("Using external Kubernetes configuration from file", "path", adminConfigPath)
|
||||
case utils.FileExists(homeConfigPath):
|
||||
clientConfig, err = k8s.NewRestConfig(homeConfigPath, false, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create k8s clientset from external file: %q: %v", homeConfigPath, err)
|
||||
return nil, fmt.Errorf("could not create k8s REST config from external file: %q: %w", homeConfigPath, err)
|
||||
}
|
||||
log.Debugf("Using external Kubernetes configuration from file [%s]", homeConfigPath)
|
||||
clientset, err = k8s.NewClientset(clientConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create k8s clientset from external file: %q: %w", homeConfigPath, err)
|
||||
}
|
||||
log.Debug("Using external Kubernetes configuration from file", "path", adminConfigPath)
|
||||
default:
|
||||
clientset, err = k8s.NewClientset("", true, "")
|
||||
clientConfig, err = k8s.NewRestConfig("", true, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create k8s clientset from incluster config: %v", err)
|
||||
return nil, fmt.Errorf("could not create k8s REST config from incluster file: %q: %w", homeConfigPath, err)
|
||||
}
|
||||
clientset, err = k8s.NewClientset(clientConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create k8s clientset from incluster config: %w", err)
|
||||
}
|
||||
log.Debug("Using external Kubernetes configuration from incluster config.")
|
||||
}
|
||||
|
||||
var rwClientSet *kubernetes.Clientset
|
||||
// if clientConfig is not nil, then we are not using etcd leader election
|
||||
// we need to create non-timeout clientset for RetryWatcher
|
||||
if clientConfig != nil {
|
||||
rwConfig := *clientConfig
|
||||
rwConfig.Timeout = 0
|
||||
rwClientSet, err = k8s.NewClientset(&rwConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create k8s clientset for retry watcher: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Flip this to something else
|
||||
// if config.DetectControlPlane {
|
||||
// log.Info("[k8s client] flipping to internal service account")
|
||||
@@ -119,10 +177,41 @@ func New(configMap string, config *kubevip.Config) (*Manager, error) {
|
||||
// }
|
||||
// }
|
||||
|
||||
// listen for interrupts or the Linux SIGTERM signal and cancel
|
||||
// our context, which the leader election code will observe and
|
||||
// step down
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
// Add Notification for Userland interrupt
|
||||
signal.Notify(signalChan, syscall.SIGINT)
|
||||
|
||||
// Add Notification for SIGTERM (sent from Kubernetes)
|
||||
signal.Notify(signalChan, syscall.SIGTERM)
|
||||
|
||||
// All watchers and other goroutines should have an additional goroutine that blocks on this, to shut things down
|
||||
shutdownChan := make(chan struct{})
|
||||
|
||||
intfMgr := networkinterface.NewManager()
|
||||
arpMgr := arp.NewManager(config)
|
||||
|
||||
// create the node label manager
|
||||
// constructor will decide if it should be a noop or not
|
||||
nodeLabelManager := node.NewManager(config, clientset)
|
||||
|
||||
var bgpServer *bgp.Server
|
||||
if config.EnableBGP {
|
||||
bgpServer, err = bgp.NewBGPServer(config.BGPConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating BGP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
svcProcessor := services.NewServicesProcessor(config, bgpServer, clientset, rwClientSet, shutdownChan, intfMgr, arpMgr, nodeLabelManager)
|
||||
|
||||
return &Manager{
|
||||
clientSet: clientset,
|
||||
configMap: configMap,
|
||||
config: config,
|
||||
clientSet: clientset,
|
||||
rwClientSet: rwClientSet,
|
||||
configMap: configMap,
|
||||
config: config,
|
||||
countServiceWatchEvent: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "kube_vip",
|
||||
Subsystem: "manager",
|
||||
@@ -135,12 +224,18 @@ func New(configMap string, config *kubevip.Config) (*Manager, error) {
|
||||
Name: "bgp_session_info",
|
||||
Help: "Display state of session by setting metric for label value with current state to 1",
|
||||
}, []string{"state", "peer"}),
|
||||
signalChan: signalChan,
|
||||
shutdownChan: shutdownChan,
|
||||
svcProcessor: svcProcessor,
|
||||
intfMgr: intfMgr,
|
||||
arpMgr: arpMgr,
|
||||
bgpServer: bgpServer,
|
||||
nodeLabelManager: nodeLabelManager,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start will begin the Manager, which will start services and watch the configmap
|
||||
func (sm *Manager) Start() error {
|
||||
|
||||
// listen for interrupts or the Linux SIGTERM signal and cancel
|
||||
// our context, which the leader election code will observe and
|
||||
// step down
|
||||
@@ -151,9 +246,39 @@ func (sm *Manager) Start() error {
|
||||
// Add Notification for SIGTERM (sent from Kubernetes)
|
||||
signal.Notify(sm.signalChan, syscall.SIGTERM)
|
||||
|
||||
// Add Notification for SIGUSR1 (for configuration dump)
|
||||
signal.Notify(sm.signalChan, syscall.SIGUSR1)
|
||||
|
||||
// All watchers and other goroutines should have an additional goroutine that blocks on this, to shut things down
|
||||
sm.shutdownChan = make(chan struct{})
|
||||
|
||||
// HealthCheck
|
||||
if sm.config.HealthCheckPort != 0 {
|
||||
if sm.config.HealthCheckPort < 1024 {
|
||||
return fmt.Errorf("healthcheck port is using a port that is less than 1024 [%d]", sm.config.HealthCheckPort)
|
||||
}
|
||||
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
fmt.Fprintf(w, "OK")
|
||||
})
|
||||
go func() {
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", sm.config.HealthCheckPort),
|
||||
ReadHeaderTimeout: 3 * time.Second,
|
||||
}
|
||||
err := server.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Error("healthcheck", "unable to start", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// on exit, clean up the node labels
|
||||
defer func() {
|
||||
if err := sm.nodeLabelManager.CleanUpLabels(10 * time.Second); err != nil {
|
||||
log.Error("CleanUpNodeLabels", "unable to cleanup node labels", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// If BGP is enabled then we start a server instance that will broadcast VIPs
|
||||
if sm.config.EnableBGP {
|
||||
|
||||
@@ -163,27 +288,48 @@ func (sm *Manager) Start() error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infoln("Starting Kube-vip Manager with the BGP engine")
|
||||
log.Info("Starting Kube-vip Manager with the BGP engine")
|
||||
return sm.startBGP()
|
||||
}
|
||||
|
||||
if sm.config.EnableARP || sm.config.EnableWireguard {
|
||||
if sm.config.EnableUPNP {
|
||||
clients := upnp.GetConnectionClients(context.TODO())
|
||||
if len(clients) == 0 {
|
||||
log.Error("Error Enabling UPNP. No Clients found")
|
||||
// Set the struct to false so nothing should use it in future
|
||||
sm.config.EnableUPNP = false
|
||||
} else {
|
||||
for _, c := range clients {
|
||||
ip, err := c.GetExternalIPAddress()
|
||||
if err != nil {
|
||||
log.Error("unable to find IGD2 Gateway address", "err", err)
|
||||
}
|
||||
log.Info("Found UPNP IGD2 Gateway address", "ip", ip)
|
||||
}
|
||||
}
|
||||
// TODO: It would be nice to run the UPNP refresh only on the leader.
|
||||
go sm.svcProcessor.RefreshUPNPForwards()
|
||||
}
|
||||
}
|
||||
|
||||
// If ARP is enabled then we start a LeaderElection that will use ARP to advertise VIPs
|
||||
if sm.config.EnableARP {
|
||||
log.Infoln("Starting Kube-vip Manager with the ARP engine")
|
||||
return sm.startARP()
|
||||
log.Info("Starting Kube-vip Manager with the ARP engine")
|
||||
return sm.startARP(sm.config.NodeName)
|
||||
}
|
||||
|
||||
if sm.config.EnableWireguard {
|
||||
log.Infoln("Starting Kube-vip Manager with the Wireguard engine")
|
||||
return sm.startWireguard()
|
||||
log.Info("Starting Kube-vip Manager with the Wireguard engine")
|
||||
return sm.startWireguard(sm.config.NodeName)
|
||||
}
|
||||
|
||||
if sm.config.EnableRoutingTable {
|
||||
log.Infoln("Starting Kube-vip Manager with the Routing Table engine")
|
||||
return sm.startTableMode()
|
||||
log.Info("Starting Kube-vip Manager with the Routing Table engine")
|
||||
return sm.startTableMode(sm.config.NodeName)
|
||||
}
|
||||
|
||||
log.Errorln("prematurely exiting Load-balancer as no modes [ARP/BGP/Wireguard] are enabled")
|
||||
log.Error("prematurely exiting Load-balancer as no modes [ARP/BGP/Wireguard] are enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -199,7 +345,7 @@ func returnNameSpace() (string, error) {
|
||||
|
||||
func (sm *Manager) parseAnnotations() error {
|
||||
if sm.config.Annotations == "" {
|
||||
log.Debugf("No Node annotations to parse")
|
||||
log.Debug("No Node annotations to parse")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -209,23 +355,3 @@ func (sm *Manager) parseAnnotations() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return !info.IsDir()
|
||||
}
|
||||
|
||||
func (sm *Manager) findServiceInstance(svc *v1.Service) *Instance {
|
||||
svcUID := string(svc.UID)
|
||||
log.Debugf("service UID: %s", svcUID)
|
||||
for i := range sm.serviceInstances {
|
||||
log.Debugf("saved service instance %d UID: %s", i, sm.serviceInstances[i].UID)
|
||||
if sm.serviceInstances[i].UID == svcUID {
|
||||
return sm.serviceInstances[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/kamhlos/upnp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "log/slog"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
@@ -19,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
// Start will begin the Manager, which will start services and watch the configmap
|
||||
func (sm *Manager) startARP() error {
|
||||
func (sm *Manager) startARP(id string) error {
|
||||
var cpCluster *cluster.Cluster
|
||||
var ns string
|
||||
var err error
|
||||
@@ -29,21 +27,33 @@ func (sm *Manager) startARP() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
log.Info("Start ARP/NDP advertisement")
|
||||
go sm.arpMgr.StartAdvertisement(ctx)
|
||||
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-sm.signalChan
|
||||
log.Info("Received kube-vip termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster.Stop()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received kube-vip termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster.Stop()
|
||||
}
|
||||
// Close all go routines
|
||||
close(sm.shutdownChan)
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Close all go routines
|
||||
close(sm.shutdownChan)
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster, err = cluster.InitCluster(sm.config, false)
|
||||
cpCluster, err = cluster.InitCluster(sm.config, false, sm.intfMgr, sm.arpMgr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -56,17 +66,16 @@ func (sm *Manager) startARP() error {
|
||||
go func() {
|
||||
err := cpCluster.StartCluster(sm.config, clusterManager, nil)
|
||||
if err != nil {
|
||||
log.Errorf("Control Plane Error [%v]", err)
|
||||
log.Error("starting control plane", "err", err)
|
||||
// Trigger the shutdown of this manager instance
|
||||
sm.signalChan <- syscall.SIGINT
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
// Check if we're also starting the services, if not we can sit and wait on the closing channel and return here
|
||||
if !sm.config.EnableServices {
|
||||
<-sm.signalChan
|
||||
log.Infof("Shutting down Kube-Vip")
|
||||
<-sm.shutdownChan
|
||||
log.Info("Shutting down Kube-Vip")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -76,57 +85,27 @@ func (sm *Manager) startARP() error {
|
||||
|
||||
ns, err = returnNameSpace()
|
||||
if err != nil {
|
||||
log.Warnf("unable to auto-detect namespace, dropping to [%s]", sm.config.Namespace)
|
||||
log.Warn("unable to auto-detect namespace, dropping to config", "namespace", sm.config.Namespace)
|
||||
ns = sm.config.Namespace
|
||||
}
|
||||
}
|
||||
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Before starting the leader Election enable any additional functionality
|
||||
upnpEnabled, _ := strconv.ParseBool(os.Getenv("enableUPNP"))
|
||||
|
||||
if upnpEnabled {
|
||||
sm.upnp = new(upnp.Upnp)
|
||||
err := sm.upnp.ExternalIPAddr()
|
||||
if err != nil {
|
||||
log.Errorf("Error Enabling UPNP %s", err.Error())
|
||||
// Set the struct to nil so nothing should use it in future
|
||||
sm.upnp = nil
|
||||
} else {
|
||||
log.Infof("Successfully enabled UPNP, Gateway address [%s]", sm.upnp.GatewayOutsideIP)
|
||||
}
|
||||
}
|
||||
|
||||
// This will tidy any dangling kube-vip iptables rules
|
||||
if os.Getenv("EGRESS_CLEAN") != "" {
|
||||
i, err := vip.CreateIptablesClient(sm.config.EgressWithNftables, sm.config.ServiceNamespace, iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
log.Warnf("(egress) Unable to clean any dangling egress rules [%v]", err)
|
||||
log.Warn("(egress) Can be ignored in non iptables release of kube-vip")
|
||||
} else {
|
||||
log.Info("(egress) Cleaning any dangling kube-vip egress rules")
|
||||
cleanErr := i.CleanIPtables()
|
||||
if cleanErr != nil {
|
||||
log.Errorf("Error cleaning rules [%v]", cleanErr)
|
||||
}
|
||||
}
|
||||
if sm.config.EgressClean {
|
||||
vip.ClearIPTables(sm.config.EgressWithNftables, sm.config.ServiceNamespace, iptables.ProtocolIPv4)
|
||||
}
|
||||
|
||||
// Start a services watcher (all kube-vip pods will watch services), upon a new service
|
||||
// a lock based upon that service is created that they will all leaderElection on
|
||||
if sm.config.EnableServicesElection {
|
||||
log.Infof("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.startServicesWatchForLeaderElection(ctx)
|
||||
log.Info("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
log.Infof("beginning services leadership, namespace [%s], lock name [%s], id [%s]", ns, sm.config.ServicesLeaseName, id)
|
||||
log.Info("beginning services leadership", "namespace", ns, "lock name", sm.config.ServicesLeaseName, "id", id)
|
||||
// we use the Lease lock type since edits to Leases are less common
|
||||
// and fewer objects in the cluster watch "all Leases".
|
||||
lock := &resourcelock.LeaseLock{
|
||||
@@ -155,21 +134,21 @@ func (sm *Manager) startARP() error {
|
||||
RetryPeriod: time.Duration(sm.config.RetryPeriod) * time.Second,
|
||||
Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
err = sm.servicesWatcher(ctx, sm.syncServices)
|
||||
err = sm.svcProcessor.ServicesWatcher(ctx, sm.svcProcessor.SyncServices)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Error("service watcher", "err", err)
|
||||
panic("") // TODO: - emulating log.fatal here
|
||||
}
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
// we can do cleanup here
|
||||
log.Infof("leader lost: %s", id)
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.clusters {
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
log.Info("leader lost", "new leader", id)
|
||||
sm.svcProcessor.Stop()
|
||||
|
||||
log.Fatal("lost leadership, restarting kube-vip")
|
||||
log.Error("lost leadership, restarting kube-vip")
|
||||
panic("") // TODO: - emulating log.fatal here
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
@@ -180,7 +159,7 @@ func (sm *Manager) startARP() error {
|
||||
// I just got the lock
|
||||
return
|
||||
}
|
||||
log.Infof("new leader elected: %s", identity)
|
||||
log.Info("new leader elected", "new leader", identity)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,16 +3,14 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/equinixmetal"
|
||||
api "github.com/osrg/gobgp/v3/api"
|
||||
"github.com/packethost/packngo"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Start will begin the Manager, which will start services and watch the configmap
|
||||
@@ -21,37 +19,15 @@ func (sm *Manager) startBGP() error {
|
||||
// var ns string
|
||||
var err error
|
||||
|
||||
// If Equinix Metal is enabled then we can begin our preparation work
|
||||
var packetClient *packngo.Client
|
||||
if sm.config.EnableMetal {
|
||||
if sm.config.ProviderConfig != "" {
|
||||
key, project, err := equinixmetal.GetPacketConfig(sm.config.ProviderConfig)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
// Set the environment variable with the key for the project
|
||||
os.Setenv("PACKET_AUTH_TOKEN", key)
|
||||
// Update the configuration with the project key
|
||||
sm.config.MetalProjectID = project
|
||||
}
|
||||
}
|
||||
packetClient, err = packngo.NewClient()
|
||||
if sm.bgpServer == nil {
|
||||
sm.bgpServer, err = bgp.NewBGPServer(sm.config.BGPConfig)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
// We're using Equinix Metal with BGP, populate the Peer information from the API
|
||||
if sm.config.EnableBGP {
|
||||
log.Infoln("Looking up the BGP configuration from Equinix Metal")
|
||||
err = equinixmetal.BGPLookup(packetClient, sm.config)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return fmt.Errorf("creating BGP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Starting the BGP server to advertise VIP routes to BGP peers")
|
||||
sm.bgpServer, err = bgp.NewBGPServer(&sm.config.BGPConfig, func(p *api.WatchEventResponse_PeerEvent) {
|
||||
if err := sm.bgpServer.Start(func(p *api.WatchEventResponse_PeerEvent) {
|
||||
ipaddr := p.GetPeer().GetState().GetNeighborAddress()
|
||||
port := uint64(179)
|
||||
peerDescription := fmt.Sprintf("%s:%d", ipaddr, port)
|
||||
@@ -67,9 +43,8 @@ func (sm *Manager) startBGP() error {
|
||||
"peer": peerDescription,
|
||||
}).Set(metricValue)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("starting BGP server: %w", err)
|
||||
}
|
||||
|
||||
// use a Go context so we can tell the leaderelection code when we
|
||||
@@ -86,19 +61,28 @@ func (sm *Manager) startBGP() error {
|
||||
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-sm.signalChan
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
if cpCluster != nil {
|
||||
cpCluster.Stop()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
if cpCluster != nil {
|
||||
cpCluster.Stop()
|
||||
}
|
||||
}
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster, err = cluster.InitCluster(sm.config, false)
|
||||
cpCluster, err = cluster.InitCluster(sm.config, false, sm.intfMgr, sm.arpMgr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -112,10 +96,10 @@ func (sm *Manager) startBGP() error {
|
||||
if sm.config.EnableLeaderElection {
|
||||
err = cpCluster.StartCluster(sm.config, clusterManager, sm.bgpServer)
|
||||
} else {
|
||||
err = cpCluster.StartVipService(sm.config, clusterManager, sm.bgpServer, packetClient)
|
||||
err = cpCluster.StartVipService(sm.config, clusterManager, sm.bgpServer)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Control Plane Error [%v]", err)
|
||||
log.Error("Control Plane", "err", err)
|
||||
// Trigger the shutdown of this manager instance
|
||||
sm.signalChan <- syscall.SIGINT
|
||||
}
|
||||
@@ -124,18 +108,27 @@ func (sm *Manager) startBGP() error {
|
||||
// Check if we're also starting the services, if not we can sit and wait on the closing channel and return here
|
||||
if !sm.config.EnableServices {
|
||||
<-sm.signalChan
|
||||
log.Infof("Shutting down Kube-Vip")
|
||||
log.Info("Shutting down Kube-Vip")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err = sm.servicesWatcher(ctx, sm.syncServices)
|
||||
if err != nil {
|
||||
return err
|
||||
if sm.config.EnableServicesElection {
|
||||
log.Info("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("beginning watching services without leader election")
|
||||
err = sm.svcProcessor.ServicesWatcher(ctx, sm.svcProcessor.SyncServices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Shutting down Kube-Vip")
|
||||
log.Info("Shutting down Kube-Vip")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
235
pkg/manager/manager_dump.go
Normal file
235
pkg/manager/manager_dump.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/nftables"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// dumpConfiguration prints the current configuration to stdout when SIGUSR1 is received
|
||||
func (sm *Manager) dumpConfiguration() {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf(" KUBE-VIP CONFIGURATION DUMP\n")
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf("Timestamp: %s\n", time.Now().Format(time.RFC3339))
|
||||
fmt.Printf("Node Name: %s\n", sm.config.NodeName)
|
||||
fmt.Printf("Process ID: %d\n", os.Getpid())
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf("\n")
|
||||
|
||||
sm.dumpConfigSection()
|
||||
sm.dumpBGPSection()
|
||||
sm.dumpARPSection()
|
||||
sm.dumpServicesSection()
|
||||
sm.dumpNetworkInterfacesSection()
|
||||
sm.dumpLeaderElectionSection()
|
||||
sm.dumpRuntimeSection()
|
||||
sm.dumpNFTablesSection()
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf(" END OF CONFIGURATION DUMP\n")
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpConfigSection() {
|
||||
fmt.Printf("--- BASIC CONFIGURATION ---\n")
|
||||
fmt.Printf("VIP: %s\n", sm.config.Address)
|
||||
fmt.Printf("VIP Subnet: %s\n", sm.config.VIPSubnet)
|
||||
fmt.Printf("Port: %d\n", sm.config.Port)
|
||||
fmt.Printf("Namespace: %s\n", sm.config.Namespace)
|
||||
fmt.Printf("Service Namespace: %s\n", sm.config.ServiceNamespace)
|
||||
fmt.Printf("Interface: %s\n", sm.config.Interface)
|
||||
fmt.Printf("Services Interface: %s\n", sm.config.ServicesInterface)
|
||||
fmt.Printf("Single Node Mode: %t\n", sm.config.SingleNode)
|
||||
fmt.Printf("Start As Leader: %t\n", sm.config.StartAsLeader)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpBGPSection() {
|
||||
fmt.Printf("--- BGP CONFIGURATION ---\n")
|
||||
fmt.Printf("BGP Enabled: %t\n", sm.config.EnableBGP)
|
||||
if sm.config.EnableBGP {
|
||||
fmt.Printf("BGP AS: %d\n", sm.config.BGPConfig.AS)
|
||||
fmt.Printf("BGP Router ID: %s\n", sm.config.BGPConfig.RouterID)
|
||||
fmt.Printf("BGP Source IP: %s\n", sm.config.BGPConfig.SourceIP)
|
||||
fmt.Printf("BGP Source Interface: %s\n", sm.config.BGPConfig.SourceIF)
|
||||
fmt.Printf("BGP Hold Time: %d\n", sm.config.BGPConfig.HoldTime)
|
||||
fmt.Printf("BGP Keepalive Interval: %d\n", sm.config.BGPConfig.KeepaliveInterval)
|
||||
fmt.Printf("BGP Peers: %d\n", len(sm.config.BGPConfig.Peers))
|
||||
for i, peer := range sm.config.BGPConfig.Peers {
|
||||
fmt.Printf(" Peer %d: %s:%d (AS: %d, MultiHop: %t)\n",
|
||||
i+1, peer.Address, peer.Port, peer.AS, peer.MultiHop)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpARPSection() {
|
||||
fmt.Printf("--- ARP/NDP CONFIGURATION ---\n")
|
||||
fmt.Printf("ARP Enabled: %t\n", sm.config.EnableARP)
|
||||
if sm.config.EnableARP {
|
||||
fmt.Printf("ARP Broadcast Rate: %d\n", sm.config.ArpBroadcastRate)
|
||||
}
|
||||
fmt.Printf("Wireguard Enabled: %t\n", sm.config.EnableWireguard)
|
||||
fmt.Printf("Routing Table Enabled: %t\n", sm.config.EnableRoutingTable)
|
||||
if sm.config.EnableRoutingTable {
|
||||
fmt.Printf("Routing Table ID: %d\n", sm.config.RoutingTableID)
|
||||
fmt.Printf("Routing Protocol: %d\n", sm.config.RoutingProtocol)
|
||||
fmt.Printf("Clean Routing Table: %t\n", sm.config.CleanRoutingTable)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpServicesSection() {
|
||||
fmt.Printf("--- SERVICES CONFIGURATION ---\n")
|
||||
fmt.Printf("Services Enabled: %t\n", sm.config.EnableServices)
|
||||
if sm.config.EnableServices {
|
||||
fmt.Printf("Services Election: %t\n", sm.config.EnableServicesElection)
|
||||
fmt.Printf("Load Balancer Class Only: %t\n", sm.config.LoadBalancerClassOnly)
|
||||
fmt.Printf("Load Balancer Class Name: %s\n", sm.config.LoadBalancerClassName)
|
||||
fmt.Printf("Disable Service Updates: %t\n", sm.config.DisableServiceUpdates)
|
||||
fmt.Printf("Enable Endpoints: %t\n", sm.config.EnableEndpoints)
|
||||
fmt.Printf("Service Security Enabled: %t\n", sm.config.EnableServiceSecurity)
|
||||
|
||||
if sm.svcProcessor != nil {
|
||||
instances := sm.svcProcessor.ServiceInstances
|
||||
fmt.Printf("Kube-vip Active Service Instances: %d\n", len(instances))
|
||||
for i, inst := range instances {
|
||||
if inst.ServiceSnapshot != nil {
|
||||
svc := inst.ServiceSnapshot
|
||||
vipConfigs := ""
|
||||
for j, cfg := range svc.Status.LoadBalancer.Ingress {
|
||||
if j > 0 {
|
||||
vipConfigs += ", "
|
||||
}
|
||||
vipConfigs += cfg.IP
|
||||
}
|
||||
fmt.Printf(" Service %d: %s/%s (Type: %s, VIPs: %s)\n",
|
||||
i+1, svc.Namespace, svc.Name, svc.Spec.Type, vipConfigs)
|
||||
}
|
||||
}
|
||||
}
|
||||
if sm.clientSet != nil {
|
||||
fmt.Println()
|
||||
// Kubernetes configuration
|
||||
fmt.Println("--- KUBERNETES CONFIGURATION (SERVICES/ENDPOINTSLICES) ---")
|
||||
|
||||
fmt.Println("Service Configuration:")
|
||||
svcList, err := sm.clientSet.CoreV1().Services(v1.NamespaceAll).List(context.TODO(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
fmt.Println("Unable to retrieve all Services")
|
||||
} else {
|
||||
for x := range svcList.Items {
|
||||
|
||||
// Build all addresses
|
||||
vipConfigs := ""
|
||||
for j, cfg := range svcList.Items[x].Status.LoadBalancer.Ingress {
|
||||
if j > 0 {
|
||||
vipConfigs += ", "
|
||||
}
|
||||
vipConfigs += cfg.IP
|
||||
}
|
||||
fmt.Printf("Name=%s, UUID=%s, Addresses=%s\n", svcList.Items[x].Name, string(svcList.Items[x].UID), vipConfigs)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("EndpointSlice Configuration (note endpoint names have -XXXXX prefixed):")
|
||||
epList, err := sm.clientSet.DiscoveryV1().EndpointSlices(v1.NamespaceAll).List(context.TODO(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
fmt.Println("Unable to retrieve all EndpointSlices")
|
||||
} else {
|
||||
for x := range epList.Items {
|
||||
// Build all addresses
|
||||
fmt.Printf(" Endpoint Slice Name: %s\n", epList.Items[x].Name)
|
||||
for _, ep := range epList.Items[x].Endpoints {
|
||||
endpoints := ""
|
||||
for i, addresses := range ep.Addresses {
|
||||
if i > 0 {
|
||||
endpoints += ", "
|
||||
}
|
||||
endpoints += addresses
|
||||
}
|
||||
nodeName := "Unknown"
|
||||
targetPod := "Unknown"
|
||||
if ep.NodeName != nil {
|
||||
nodeName = *ep.NodeName
|
||||
}
|
||||
if ep.TargetRef != nil {
|
||||
targetPod = ep.TargetRef.Name
|
||||
}
|
||||
fmt.Printf("\tNode: %s, Target Pod:%s, Addresses: %s\n", nodeName, targetPod, endpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpNetworkInterfacesSection() {
|
||||
fmt.Printf("--- NETWORK INTERFACES ---\n")
|
||||
fmt.Printf("Network Interface Manager: %t\n", sm.intfMgr != nil)
|
||||
fmt.Printf("ARP Manager: %t\n", sm.arpMgr != nil)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpLeaderElectionSection() {
|
||||
fmt.Printf("--- LEADER ELECTION CONFIGURATION ---\n")
|
||||
fmt.Printf("Control Plane Enabled: %t\n", sm.config.EnableControlPlane)
|
||||
if sm.config.EnableControlPlane {
|
||||
fmt.Printf("Detect Control Plane: %t\n", sm.config.DetectControlPlane)
|
||||
}
|
||||
fmt.Printf("Leader Election Type: %s\n", sm.config.LeaderElectionType)
|
||||
fmt.Printf("Leader Election Enabled: %t\n", sm.config.EnableLeaderElection)
|
||||
if sm.config.EnableLeaderElection {
|
||||
fmt.Printf("Lease Name: %s\n", sm.config.LeaseName)
|
||||
fmt.Printf("Lease Duration: %d seconds\n", sm.config.LeaseDuration)
|
||||
fmt.Printf("Renew Deadline: %d seconds\n", sm.config.RenewDeadline)
|
||||
fmt.Printf("Retry Period: %d seconds\n", sm.config.RetryPeriod)
|
||||
}
|
||||
fmt.Printf("Services Lease Name: %s\n", sm.config.ServicesLeaseName)
|
||||
fmt.Printf("Node Labeling Enabled: %t\n", sm.config.EnableNodeLabeling)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpRuntimeSection() {
|
||||
fmt.Printf("--- RUNTIME STATISTICS ---\n")
|
||||
fmt.Printf("Load Balancer Enabled: %t\n", sm.config.EnableLoadBalancer)
|
||||
if sm.config.EnableLoadBalancer {
|
||||
fmt.Printf("Load Balancer Port: %d\n", sm.config.LoadBalancerPort)
|
||||
fmt.Printf("Load Balancer Forwarding Method: %s\n", sm.config.LoadBalancerForwardingMethod)
|
||||
fmt.Printf("Load Balancers Configured: %d\n", len(sm.config.LoadBalancers))
|
||||
}
|
||||
fmt.Printf("Prometheus HTTP Server: %s\n", sm.config.PrometheusHTTPServer)
|
||||
fmt.Printf("Health Check Port: %d\n", sm.config.HealthCheckPort)
|
||||
fmt.Printf("UPNP Enabled: %t\n", sm.config.EnableUPNP)
|
||||
fmt.Printf("Egress Clean Enabled: %t\n", sm.config.EgressClean)
|
||||
if sm.config.EgressClean {
|
||||
fmt.Printf("Egress with nftables: %t\n", sm.config.EgressWithNftables)
|
||||
fmt.Printf("Egress Pod CIDR: %s\n", sm.config.EgressPodCidr)
|
||||
fmt.Printf("Egress Service CIDR: %s\n", sm.config.EgressServiceCidr)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpNFTablesSection() {
|
||||
fmt.Printf("--- NFTABLES CONFIGURATION ---\n")
|
||||
chains, err := nftables.ListChains()
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to retrieve NFTables chains, error=%s", err)
|
||||
}
|
||||
for x := range chains {
|
||||
fmt.Printf("Chain: %s\n", chains[x])
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
238
pkg/manager/manager_dump_test.go
Normal file
238
pkg/manager/manager_dump_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDumpConfiguration(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
Address: "192.168.1.100",
|
||||
Interface: "eth0",
|
||||
Port: 6443,
|
||||
EnableARP: true,
|
||||
EnableBGP: false,
|
||||
EnableControlPlane: true,
|
||||
EnableServices: true,
|
||||
LeaderElectionType: "kubernetes",
|
||||
Namespace: "kube-system",
|
||||
NodeName: "test-node",
|
||||
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
|
||||
EnableLeaderElection: true,
|
||||
LeaseName: "test-lease",
|
||||
},
|
||||
}
|
||||
|
||||
mgr := &Manager{
|
||||
config: config,
|
||||
}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpConfiguration()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "KUBE-VIP CONFIGURATION DUMP", "should contain header")
|
||||
assert.Contains(t, output, "Node Name: test-node", "should contain node name")
|
||||
assert.Contains(t, output, "VIP: 192.168.1.100", "should contain VIP address")
|
||||
assert.Contains(t, output, "Interface: eth0", "should contain interface")
|
||||
assert.Contains(t, output, "Port: 6443", "should contain port")
|
||||
assert.Contains(t, output, "ARP Enabled: true", "should contain ARP status")
|
||||
assert.Contains(t, output, "BGP Enabled: false", "should contain BGP status")
|
||||
}
|
||||
|
||||
func TestDumpConfigSection(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
Address: "192.168.1.100",
|
||||
Interface: "eth0",
|
||||
Port: 6443,
|
||||
VIPSubnet: "/24",
|
||||
EnableARP: true,
|
||||
EnableBGP: false,
|
||||
EnableControlPlane: true,
|
||||
EnableServices: true,
|
||||
LeaderElectionType: "kubernetes",
|
||||
Namespace: "kube-system",
|
||||
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
|
||||
EnableLeaderElection: true,
|
||||
LeaseName: "test-lease",
|
||||
},
|
||||
}
|
||||
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpConfigSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "--- BASIC CONFIGURATION ---")
|
||||
assert.Contains(t, output, "VIP: 192.168.1.100")
|
||||
assert.Contains(t, output, "VIP Subnet: /24")
|
||||
assert.Contains(t, output, "Interface: eth0")
|
||||
assert.Contains(t, output, "Port: 6443")
|
||||
assert.Contains(t, output, "Namespace: kube-system")
|
||||
assert.Contains(t, output, "Single Node Mode: false")
|
||||
assert.Contains(t, output, "Start As Leader: false")
|
||||
}
|
||||
|
||||
func TestDumpBGPSection(t *testing.T) {
|
||||
t.Run("BGP disabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableBGP: false,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpBGPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "BGP Enabled: false")
|
||||
})
|
||||
|
||||
t.Run("BGP enabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableBGP: true,
|
||||
BGPConfig: kubevip.BGPConfig{
|
||||
RouterID: "192.168.1.1",
|
||||
AS: 65000,
|
||||
Peers: []kubevip.BGPPeer{
|
||||
{Address: "192.168.1.2", AS: 65001},
|
||||
{Address: "192.168.1.3", AS: 65002},
|
||||
},
|
||||
},
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpBGPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "BGP Enabled: true")
|
||||
assert.Contains(t, output, "BGP Router ID: 192.168.1.1")
|
||||
assert.Contains(t, output, "BGP AS: 65000")
|
||||
assert.Contains(t, output, "BGP Peers: 2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDumpARPSection(t *testing.T) {
|
||||
t.Run("ARP disabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableARP: false,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpARPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "ARP Enabled: false")
|
||||
})
|
||||
|
||||
t.Run("ARP enabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableARP: true,
|
||||
ArpBroadcastRate: 5,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpARPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "ARP Enabled: true")
|
||||
assert.Contains(t, output, "ARP Broadcast Rate: 5")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDumpRuntimeSection(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableLoadBalancer: false,
|
||||
PrometheusHTTPServer: "",
|
||||
HealthCheckPort: 0,
|
||||
EnableUPNP: false,
|
||||
EgressClean: false,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpRuntimeSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "--- RUNTIME STATISTICS ---", "should contain runtime section header")
|
||||
assert.Contains(t, output, "Load Balancer Enabled: false", "should contain load balancer status")
|
||||
assert.Contains(t, output, "UPNP Enabled: false", "should contain UPNP status")
|
||||
}
|
||||
@@ -3,11 +3,15 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints"
|
||||
"github.com/kube-vip/kube-vip/pkg/iptables"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vishvananda/netlink"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
@@ -15,121 +19,164 @@ import (
|
||||
)
|
||||
|
||||
// Start will begin the Manager, which will start services and watch the configmap
|
||||
func (sm *Manager) startTableMode() error {
|
||||
var ns string
|
||||
func (sm *Manager) startTableMode(id string) error {
|
||||
var cpCluster *cluster.Cluster
|
||||
var err error
|
||||
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// use a Go context so we can tell the leaderelection code when we
|
||||
// want to step down
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
log.Infof("all routing table entries will exist in table [%d] with protocol [%d]", sm.config.RoutingTableID, sm.config.RoutingProtocol)
|
||||
log.Info("destination for routes", "table", sm.config.RoutingTableID, "protocol", sm.config.RoutingProtocol)
|
||||
|
||||
if sm.config.CleanRoutingTable {
|
||||
go func() {
|
||||
// we assume that after 10s all services should be configured so we can delete redundant routes
|
||||
time.Sleep(time.Second * 10)
|
||||
if err := sm.cleanRoutes(); err != nil {
|
||||
log.Errorf("error checking for old routes: %v", err)
|
||||
log.Error("error checking for old routes", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if sm.config.EgressClean {
|
||||
vip.ClearIPTables(sm.config.EgressWithNftables, sm.config.ServiceNamespace, iptables.ProtocolIPv4)
|
||||
vip.ClearIPTables(sm.config.EgressWithNftables, sm.config.ServiceNamespace, iptables.ProtocolIPv6)
|
||||
log.Debug("IPtables rules cleaned on startup")
|
||||
}
|
||||
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-sm.signalChan
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received kube-vip termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster.Stop()
|
||||
}
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ns, err = returnNameSpace()
|
||||
if err != nil {
|
||||
log.Warnf("unable to auto-detect namespace, dropping to [%s]", sm.config.Namespace)
|
||||
ns = sm.config.Namespace
|
||||
}
|
||||
|
||||
// Start a services watcher (all kube-vip pods will watch services), upon a new service
|
||||
// a lock based upon that service is created that they will all leaderElection on
|
||||
if sm.config.EnableServicesElection {
|
||||
log.Infof("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.startServicesWatchForLeaderElection(ctx)
|
||||
if sm.config.EnableServices {
|
||||
log.Debug("starting Services")
|
||||
ns, err := returnNameSpace()
|
||||
if err != nil {
|
||||
return err
|
||||
log.Warn("unable to auto-detect namespace", "dropping to", sm.config.Namespace)
|
||||
ns = sm.config.Namespace
|
||||
}
|
||||
} else if sm.config.EnableLeaderElection {
|
||||
|
||||
log.Infof("beginning services leadership, namespace [%s], lock name [%s], id [%s]", ns, plunderLock, id)
|
||||
// we use the Lease lock type since edits to Leases are less common
|
||||
// and fewer objects in the cluster watch "all Leases".
|
||||
lock := &resourcelock.LeaseLock{
|
||||
LeaseMeta: metav1.ObjectMeta{
|
||||
Name: plunderLock,
|
||||
Namespace: ns,
|
||||
},
|
||||
Client: sm.clientSet.CoordinationV1(),
|
||||
LockConfig: resourcelock.ResourceLockConfig{
|
||||
Identity: id,
|
||||
},
|
||||
}
|
||||
// start the leader election code loop
|
||||
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
|
||||
Lock: lock,
|
||||
// IMPORTANT: you MUST ensure that any code you have that
|
||||
// is protected by the lease must terminate **before**
|
||||
// you call cancel. Otherwise, you could have a background
|
||||
// loop still running and another process could
|
||||
// get elected before your background loop finished, violating
|
||||
// the stated goal of the lease.
|
||||
ReleaseOnCancel: true,
|
||||
LeaseDuration: time.Duration(sm.config.LeaseDuration) * time.Second,
|
||||
RenewDeadline: time.Duration(sm.config.RenewDeadline) * time.Second,
|
||||
RetryPeriod: time.Duration(sm.config.RetryPeriod) * time.Second,
|
||||
Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
err = sm.servicesWatcher(ctx, sm.syncServices)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// Start a services watcher (all kube-vip pods will watch services), upon a new service
|
||||
// a lock based upon that service is created that they will all leaderElection on
|
||||
if sm.config.EnableServicesElection {
|
||||
log.Info("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if sm.config.EnableLeaderElection {
|
||||
|
||||
log.Info("beginning services leadership", "namespace", ns, "lock name", plunderLock, "id", id)
|
||||
// we use the Lease lock type since edits to Leases are less common
|
||||
// and fewer objects in the cluster watch "all Leases".
|
||||
lock := &resourcelock.LeaseLock{
|
||||
LeaseMeta: metav1.ObjectMeta{
|
||||
Name: plunderLock,
|
||||
Namespace: ns,
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
// we can do cleanup here
|
||||
log.Infof("leader lost: %s", id)
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.clusters {
|
||||
cluster.Stop()
|
||||
Client: sm.clientSet.CoordinationV1(),
|
||||
LockConfig: resourcelock.ResourceLockConfig{
|
||||
Identity: id,
|
||||
},
|
||||
}
|
||||
// start the leader election code loop
|
||||
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
|
||||
Lock: lock,
|
||||
// IMPORTANT: you MUST ensure that any code you have that
|
||||
// is protected by the lease must terminate **before**
|
||||
// you call cancel. Otherwise, you could have a background
|
||||
// loop still running and another process could
|
||||
// get elected before your background loop finished, violating
|
||||
// the stated goal of the lease.
|
||||
ReleaseOnCancel: true,
|
||||
LeaseDuration: time.Duration(sm.config.LeaseDuration) * time.Second,
|
||||
RenewDeadline: time.Duration(sm.config.RenewDeadline) * time.Second,
|
||||
RetryPeriod: time.Duration(sm.config.RetryPeriod) * time.Second,
|
||||
Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
err = sm.svcProcessor.ServicesWatcher(ctx, sm.svcProcessor.SyncServices)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
panic("")
|
||||
}
|
||||
}
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
// we can do cleanup here
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
log.Info("leader lost", "id", id)
|
||||
sm.svcProcessor.Stop()
|
||||
|
||||
log.Fatal("lost leadership, restarting kube-vip")
|
||||
log.Error("lost leadership, restarting kube-vip")
|
||||
panic("")
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
if identity == id {
|
||||
// I just got the lock
|
||||
return
|
||||
}
|
||||
log.Info("new leader elected", "id", identity)
|
||||
},
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
if identity == id {
|
||||
// I just got the lock
|
||||
return
|
||||
}
|
||||
log.Infof("new leader elected: %s", identity)
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
log.Infof("beginning watching services without leader election")
|
||||
err = sm.servicesWatcher(ctx, sm.syncServices)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot watch services, %v", err)
|
||||
})
|
||||
} else {
|
||||
log.Info("beginning watching services without leader election")
|
||||
err = sm.svcProcessor.ServicesWatcher(ctx, sm.svcProcessor.SyncServices)
|
||||
if err != nil {
|
||||
log.Error("Cannot watch services", "err", err)
|
||||
} else {
|
||||
log.Debug("watching services")
|
||||
}
|
||||
}
|
||||
}
|
||||
if sm.config.EnableControlPlane {
|
||||
log.Debug("initCluster for ControlPlane")
|
||||
cpCluster, err = cluster.InitCluster(sm.config, false, sm.intfMgr, sm.arpMgr)
|
||||
if err != nil {
|
||||
log.Debug("init of ControlPlane NOT successful")
|
||||
return fmt.Errorf("cluster initialization error: %w", err)
|
||||
}
|
||||
log.Debug("init of ControlPlane successful")
|
||||
log.Debug("init ClusterManager")
|
||||
clusterManager, err := initClusterManager(sm)
|
||||
if err != nil {
|
||||
log.Debug("init cluster manager NOT successful")
|
||||
return fmt.Errorf("cluster manager initialization error: %w", err)
|
||||
}
|
||||
log.Debug("init ClusterManager successful")
|
||||
if err := cpCluster.StartVipService(sm.config, clusterManager, nil); err != nil {
|
||||
log.Error("Control Plane", "err", err)
|
||||
// Trigger the shutdown of this manager instance
|
||||
sm.signalChan <- syscall.SIGINT
|
||||
} else {
|
||||
log.Debug("start VipServer for cluster manager successful")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) cleanRoutes() error {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
routes, err := vip.ListRoutes(sm.config.RoutingTableID, sm.config.RoutingProtocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting routes: %w", err)
|
||||
@@ -137,23 +184,20 @@ func (sm *Manager) cleanRoutes() error {
|
||||
|
||||
for i := range routes {
|
||||
found := false
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.clusters {
|
||||
for n := range cluster.Network {
|
||||
r := cluster.Network[n].PrepareRoute()
|
||||
if r.Dst.String() == routes[i].Dst.String() {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if sm.config.EnableControlPlane {
|
||||
found = (routes[i].Dst.IP.String() == sm.config.Address)
|
||||
} else {
|
||||
found = endpoints.CountRouteReferences(&routes[i], &sm.svcProcessor.ServiceInstances) > 0
|
||||
}
|
||||
|
||||
if !found {
|
||||
err = netlink.RouteDel(&(routes[i]))
|
||||
if err != nil {
|
||||
log.Errorf("[route] error deleting route: %v", routes[i])
|
||||
log.Error("[route] deletion", "route", routes[i], "err", err)
|
||||
}
|
||||
log.Debugf("[route] deleted route: %v", routes[i])
|
||||
log.Debug("[route] deletion", "route", routes[i])
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,33 +2,27 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/kamhlos/upnp"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/wireguard"
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
)
|
||||
|
||||
// Start will begin the Manager, which will start services and watch the configmap
|
||||
func (sm *Manager) startWireguard() error {
|
||||
func (sm *Manager) startWireguard(id string) error {
|
||||
var ns string
|
||||
var err error
|
||||
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// use a Go context so we can tell the leaderelection code when we
|
||||
// want to step down
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
log.Infoln("reading wireguard peer configuration from Kubernetes secret")
|
||||
log.Info("reading wireguard peer configuration from Kubernetes secret")
|
||||
s, err := sm.clientSet.CoreV1().Secrets(sm.config.Namespace).Get(ctx, "wireguard", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -46,45 +40,38 @@ func (sm *Manager) startWireguard() error {
|
||||
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-sm.signalChan
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ns, err = returnNameSpace()
|
||||
if err != nil {
|
||||
log.Warnf("unable to auto-detect namespace, dropping to [%s]", sm.config.Namespace)
|
||||
log.Warn("unable to auto-detect namespace", "dropping to", sm.config.Namespace)
|
||||
ns = sm.config.Namespace
|
||||
}
|
||||
|
||||
// Before starting the leader Election enable any additional functionality
|
||||
upnpEnabled, _ := strconv.ParseBool(os.Getenv("enableUPNP"))
|
||||
|
||||
if upnpEnabled {
|
||||
sm.upnp = new(upnp.Upnp)
|
||||
err := sm.upnp.ExternalIPAddr()
|
||||
if err != nil {
|
||||
log.Errorf("Error Enabling UPNP %s", err.Error())
|
||||
// Set the struct to nil so nothing should use it in future
|
||||
sm.upnp = nil
|
||||
} else {
|
||||
log.Infof("Successfully enabled UPNP, Gateway address [%s]", sm.upnp.GatewayOutsideIP)
|
||||
}
|
||||
}
|
||||
|
||||
// Start a services watcher (all kube-vip pods will watch services), upon a new service
|
||||
// a lock based upon that service is created that they will all leaderElection on
|
||||
if sm.config.EnableServicesElection {
|
||||
log.Infof("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.startServicesWatchForLeaderElection(ctx)
|
||||
log.Info("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
||||
log.Infof("beginning services leadership, namespace [%s], lock name [%s], id [%s]", ns, plunderLock, id)
|
||||
log.Info("beginning services leadership", "namespace", ns, "lock name", plunderLock, "id", id)
|
||||
// we use the Lease lock type since edits to Leases are less common
|
||||
// and fewer objects in the cluster watch "all Leases".
|
||||
lock := &resourcelock.LeaseLock{
|
||||
@@ -113,21 +100,21 @@ func (sm *Manager) startWireguard() error {
|
||||
RetryPeriod: time.Duration(sm.config.RetryPeriod) * time.Second,
|
||||
Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
err = sm.servicesWatcher(ctx, sm.syncServices)
|
||||
err = sm.svcProcessor.ServicesWatcher(ctx, sm.svcProcessor.SyncServices)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Error(err.Error())
|
||||
panic("")
|
||||
}
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
// we can do cleanup here
|
||||
log.Infof("leader lost: %s", id)
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.clusters {
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
log.Info("leader lost", "id", id)
|
||||
sm.svcProcessor.Stop()
|
||||
|
||||
log.Fatal("lost leadership, restarting kube-vip")
|
||||
log.Error("lost leadership, restarting kube-vip")
|
||||
panic("")
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
@@ -135,7 +122,7 @@ func (sm *Manager) startWireguard() error {
|
||||
// I just got the lock
|
||||
return
|
||||
}
|
||||
log.Infof("new leader elected: %s", identity)
|
||||
log.Info("new leader elected", "id", identity)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -5,17 +5,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
nodeLabelIndex = "kube-vip.io/has-ip"
|
||||
nodeLabelJSONPath = `kube-vip.io~1has-ip`
|
||||
)
|
||||
|
||||
type patchStringLabel struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
@@ -28,24 +25,23 @@ func applyNodeLabel(clientSet *kubernetes.Clientset, address, id, identity strin
|
||||
ctx := context.Background()
|
||||
node, err := clientSet.CoreV1().Nodes().Get(ctx, id, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("can't query node %s labels. error: %v", id, err)
|
||||
log.Error("can't query node labels", "node", id, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("node %s labels: %+v", id, node.Labels)
|
||||
log.Debug(fmt.Sprintf("node %s labels: %+v", id, node.Labels))
|
||||
|
||||
value, ok := node.Labels[nodeLabelIndex]
|
||||
path := fmt.Sprintf("/metadata/labels/%s", nodeLabelJSONPath)
|
||||
if (!ok || value != address) && id == identity {
|
||||
log.Debugf("setting node label `has-ip=%s` on %s", address, id)
|
||||
// Append label
|
||||
applyPatchLabels(ctx, clientSet, id, "add", path, address)
|
||||
} else if ok && value == address {
|
||||
log.Debugf("removing node label `has-ip=%s` on %s", address, id)
|
||||
value, ok := node.Labels[kubevip.HasIP]
|
||||
path := fmt.Sprintf("/metadata/labels/%s", kubevip.HasIPJSONPath)
|
||||
log.Debug(fmt.Sprintf("Received identity: %s - id: %s", identity, id))
|
||||
if ok && value == address {
|
||||
log.Debug(fmt.Sprintf("removing node label `has-ip=%s` on %s", address, id))
|
||||
// Remove label
|
||||
applyPatchLabels(ctx, clientSet, id, "remove", path, address)
|
||||
} else {
|
||||
log.Debugf("no node label change needed")
|
||||
log.Debug(fmt.Sprintf("setting node label `has-ip=%s` on %s", address, id))
|
||||
// Append label
|
||||
applyPatchLabels(ctx, clientSet, id, "add", path, address)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +55,15 @@ func applyPatchLabels(ctx context.Context, clientSet *kubernetes.Clientset,
|
||||
}}
|
||||
patchData, err := json.Marshal(patchLabels)
|
||||
if err != nil {
|
||||
log.Errorf("node patch marshaling failed. error: %v", err)
|
||||
log.Error("node patch marshaling failed", "err", err)
|
||||
return
|
||||
}
|
||||
// patch node
|
||||
node, err := clientSet.CoreV1().Nodes().Patch(ctx,
|
||||
name, types.JSONPatchType, patchData, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("can't patch node %s. error: %v", name, err)
|
||||
log.Error("node patching failed", "err", err)
|
||||
return
|
||||
}
|
||||
log.Debugf("updated node %s labels: %+v", name, node.Labels)
|
||||
log.Debug("updated", "node", name, "labels", node.Labels)
|
||||
}
|
||||
|
||||
@@ -4,5 +4,12 @@ import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
// PrometheusCollector defines a service watch event counter.
|
||||
func (sm *Manager) PrometheusCollector() []prometheus.Collector {
|
||||
return []prometheus.Collector{sm.countServiceWatchEvent, sm.bgpSessionInfoGauge}
|
||||
collectors := []prometheus.Collector{}
|
||||
if sm.svcProcessor != nil {
|
||||
collectors = append(collectors, sm.svcProcessor.CountServiceWatchEvent)
|
||||
}
|
||||
if sm.bgpServer != nil {
|
||||
collectors = append(collectors, sm.bgpServer.BGPSessionInfoGauge)
|
||||
}
|
||||
return collectors
|
||||
}
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/iptables"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// DEBUG
|
||||
const (
|
||||
defaultPodCIDR = "10.0.0.0/16"
|
||||
defaultServiceCIDR = "10.96.0.0/12"
|
||||
)
|
||||
|
||||
func (sm *Manager) iptablesCheck() error {
|
||||
file, err := os.Open("/proc/modules")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
var nat, filter, mangle bool
|
||||
for scanner.Scan() {
|
||||
line := strings.Fields(scanner.Text())
|
||||
switch line[0] {
|
||||
case "iptable_filter":
|
||||
filter = true
|
||||
case "iptable_nat":
|
||||
nat = true
|
||||
case "iptable_mangle":
|
||||
mangle = true
|
||||
}
|
||||
}
|
||||
|
||||
if !filter || !nat || !mangle {
|
||||
return fmt.Errorf("missing iptables modules -> nat [%t] -> filter [%t] mangle -> [%t]", nat, filter, mangle)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSameFamilyCidr(source, ip string) string {
|
||||
cidrs := strings.Split(source, ",")
|
||||
for _, cidr := range cidrs {
|
||||
source = cidr
|
||||
if vip.IsIPv4(cidr) == vip.IsIPv4(ip) {
|
||||
return cidr
|
||||
}
|
||||
}
|
||||
// return to the default behaviour of setting the CIDR to the first one (or only one)
|
||||
return source
|
||||
}
|
||||
|
||||
func (sm *Manager) configureEgress(vipIP, podIP, destinationPorts, namespace string) error {
|
||||
// serviceCIDR, podCIDR, err := sm.AutoDiscoverCIDRs()
|
||||
// if err != nil {
|
||||
// serviceCIDR = "10.96.0.0/12"
|
||||
// podCIDR = "10.0.0.0/16"
|
||||
// }
|
||||
|
||||
var podCidr, serviceCidr string
|
||||
|
||||
if sm.config.EgressPodCidr != "" {
|
||||
podCidr = getSameFamilyCidr(sm.config.EgressPodCidr, podIP)
|
||||
} else {
|
||||
// There's no default IPv6 pod CIDR, therefore we silently back off if CIDR s not specified.
|
||||
if !vip.IsIPv4(podIP) {
|
||||
return nil
|
||||
}
|
||||
podCidr = defaultPodCIDR
|
||||
}
|
||||
|
||||
if sm.config.EgressServiceCidr != "" {
|
||||
serviceCidr = getSameFamilyCidr(sm.config.EgressServiceCidr, vipIP)
|
||||
} else {
|
||||
// There's no default IPv6 service CIDR, therefore we silently back off if CIDR s not specified.
|
||||
if !vip.IsIPv4(vipIP) {
|
||||
return nil
|
||||
}
|
||||
serviceCidr = defaultServiceCIDR
|
||||
}
|
||||
|
||||
protocol := iptables.ProtocolIPv4
|
||||
|
||||
if vip.IsIPv6(vipIP) {
|
||||
protocol = iptables.ProtocolIPv6
|
||||
}
|
||||
|
||||
i, err := vip.CreateIptablesClient(sm.config.EgressWithNftables, namespace, protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error Creating iptables client [%s]", err)
|
||||
}
|
||||
|
||||
// Check if the kube-vip mangle chain exists, if not create it
|
||||
exists, err := i.CheckMangleChain(vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking for existence of mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
if !exists {
|
||||
err = i.CreateMangleChain(vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
err = i.AppendReturnRulesForDestinationSubnet(vip.MangleChainName, podCidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
err = i.AppendReturnRulesForDestinationSubnet(vip.MangleChainName, serviceCidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
mask := "/32"
|
||||
if !vip.IsIPv4(podIP) {
|
||||
mask = "/128"
|
||||
}
|
||||
|
||||
err = i.AppendReturnRulesForMarking(vip.MangleChainName, podIP+mask)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding marking rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
err = i.InsertMangeTableIntoPrerouting(vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding prerouting mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
if destinationPorts != "" {
|
||||
|
||||
fixedPorts := strings.Split(destinationPorts, ",")
|
||||
|
||||
for _, fixedPort := range fixedPorts {
|
||||
var proto, port string
|
||||
|
||||
data := strings.Split(fixedPort, ":")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
} else if len(data) == 1 {
|
||||
proto = "tcp"
|
||||
port = data[0]
|
||||
} else {
|
||||
proto = data[0]
|
||||
port = data[1]
|
||||
}
|
||||
|
||||
err = i.InsertSourceNatForDestinationPort(vipIP, podIP, port, proto)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding snat rules to nat chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
err = i.InsertSourceNat(vipIP, podIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding snat rules to nat chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
//_ = i.DumpChain(vip.MangleChainName)
|
||||
err = vip.DeleteExistingSessions(podIP, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) AutoDiscoverCIDRs() (serviceCIDR, podCIDR string, err error) {
|
||||
pod, err := sm.clientSet.CoreV1().Pods("kube-system").Get(context.TODO(), "kube-controller-manager", v1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for flags := range pod.Spec.Containers[0].Command {
|
||||
if strings.Contains(pod.Spec.Containers[0].Command[flags], "--cluster-cidr=") {
|
||||
podCIDR = strings.ReplaceAll(pod.Spec.Containers[0].Command[flags], "--cluster-cidr=", "")
|
||||
}
|
||||
if strings.Contains(pod.Spec.Containers[0].Command[flags], "--service-cluster-ip-range=") {
|
||||
serviceCIDR = strings.ReplaceAll(pod.Spec.Containers[0].Command[flags], "--service-cluster-ip-range=", "")
|
||||
}
|
||||
}
|
||||
if podCIDR == "" || serviceCIDR == "" {
|
||||
err = fmt.Errorf("unable to fully determine cluster CIDR configurations")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (sm *Manager) TeardownEgress(podIP, vipIP, destinationPorts, namespace string) error {
|
||||
protocol := iptables.ProtocolIPv4
|
||||
if vip.IsIPv6(podIP) {
|
||||
protocol = iptables.ProtocolIPv6
|
||||
}
|
||||
|
||||
i, err := vip.CreateIptablesClient(sm.config.EgressWithNftables, namespace, protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error Creating iptables client [%s]", err)
|
||||
}
|
||||
|
||||
// Remove the marking of egress packets
|
||||
err = i.DeleteMangleMarking(podIP, vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
|
||||
// Clear up SNAT rules
|
||||
if destinationPorts != "" {
|
||||
fixedPorts := strings.Split(destinationPorts, ",")
|
||||
|
||||
for _, fixedPort := range fixedPorts {
|
||||
var proto, port string
|
||||
|
||||
data := strings.Split(fixedPort, ":")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
} else if len(data) == 1 {
|
||||
proto = "tcp"
|
||||
port = data[0]
|
||||
} else {
|
||||
proto = data[0]
|
||||
port = data[1]
|
||||
}
|
||||
|
||||
err = i.DeleteSourceNatForDestinationPort(podIP, vipIP, port, proto)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
err = i.DeleteSourceNat(podIP, vipIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
}
|
||||
err = vip.DeleteExistingSessions(podIP, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
const (
|
||||
hwAddrKey = "kube-vip.io/hwaddr"
|
||||
requestedIP = "kube-vip.io/requestedIP"
|
||||
vipHost = "kube-vip.io/vipHost"
|
||||
egress = "kube-vip.io/egress"
|
||||
egressDestinationPorts = "kube-vip.io/egress-destination-ports"
|
||||
egressSourcePorts = "kube-vip.io/egress-source-ports"
|
||||
activeEndpoint = "kube-vip.io/active-endpoint"
|
||||
activeEndpointIPv6 = "kube-vip.io/active-endpoint-ipv6"
|
||||
flushContrack = "kube-vip.io/flush-conntrack"
|
||||
loadbalancerIPAnnotation = "kube-vip.io/loadbalancerIPs"
|
||||
loadbalancerHostname = "kube-vip.io/loadbalancerHostname"
|
||||
)
|
||||
|
||||
func (sm *Manager) syncServices(_ context.Context, svc *v1.Service, wg *sync.WaitGroup) error {
|
||||
defer wg.Done()
|
||||
|
||||
log.Debugf("[STARTING] Service Sync")
|
||||
|
||||
// Iterate through the synchronising services
|
||||
foundInstance := false
|
||||
newServiceAddresses := fetchServiceAddresses(svc)
|
||||
newServiceUID := string(svc.UID)
|
||||
|
||||
ingressIPs := []string{}
|
||||
|
||||
for _, ingress := range svc.Status.LoadBalancer.Ingress {
|
||||
ingressIPs = append(ingressIPs, ingress.IP)
|
||||
}
|
||||
|
||||
shouldBreake := false
|
||||
|
||||
for x := range sm.serviceInstances {
|
||||
if shouldBreake {
|
||||
break
|
||||
}
|
||||
for _, newServiceAddress := range newServiceAddresses {
|
||||
log.Debugf("isDHCP: %t, newServiceAddress: %s", sm.serviceInstances[x].isDHCP, newServiceAddress)
|
||||
if sm.serviceInstances[x].UID == newServiceUID {
|
||||
// If the found instance's DHCP configuration doesn't match the new service, delete it.
|
||||
if (sm.serviceInstances[x].isDHCP && newServiceAddress != "0.0.0.0") ||
|
||||
(!sm.serviceInstances[x].isDHCP && newServiceAddress == "0.0.0.0") ||
|
||||
(!sm.serviceInstances[x].isDHCP && len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(ingressIPs, newServiceAddress)) ||
|
||||
(len(svc.Status.LoadBalancer.Ingress) > 0 && !comparePortsAndPortStatuses(svc)) ||
|
||||
(sm.serviceInstances[x].isDHCP && len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(ingressIPs, sm.serviceInstances[x].dhcpInterfaceIP)) {
|
||||
if err := sm.deleteService(newServiceUID); err != nil {
|
||||
return err
|
||||
}
|
||||
shouldBreake = true
|
||||
break
|
||||
}
|
||||
foundInstance = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This instance wasn't found, we need to add it to the manager
|
||||
if !foundInstance && len(newServiceAddresses) > 0 {
|
||||
if err := sm.addService(svc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func comparePortsAndPortStatuses(svc *v1.Service) bool {
|
||||
portsStatus := svc.Status.LoadBalancer.Ingress[0].Ports
|
||||
if len(portsStatus) != len(svc.Spec.Ports) {
|
||||
return false
|
||||
}
|
||||
for i, portSpec := range svc.Spec.Ports {
|
||||
if portsStatus[i].Port != portSpec.Port || portsStatus[i].Protocol != portSpec.Protocol {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *Manager) addService(svc *v1.Service) error {
|
||||
startTime := time.Now()
|
||||
|
||||
newService, err := NewInstance(svc, sm.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("(svcs) adding VIP [%s] for [%s/%s]", newService.VIPs, newService.serviceSnapshot.Namespace, newService.serviceSnapshot.Name)
|
||||
|
||||
for x := range newService.vipConfigs {
|
||||
newService.clusters[x].StartLoadBalancerService(newService.vipConfigs[x], sm.bgpServer)
|
||||
}
|
||||
|
||||
sm.upnpMap(newService)
|
||||
|
||||
if newService.isDHCP && len(newService.vipConfigs) == 1 {
|
||||
go func() {
|
||||
for ip := range newService.dhcpClient.IPChannel() {
|
||||
log.Debugf("IP %s may have changed", ip)
|
||||
newService.vipConfigs[0].VIP = ip
|
||||
newService.dhcpInterfaceIP = ip
|
||||
if !sm.config.DisableServiceUpdates {
|
||||
if err := sm.updateStatus(newService); err != nil {
|
||||
log.Warnf("error updating svc: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("IP update channel closed, stopping")
|
||||
}()
|
||||
}
|
||||
|
||||
sm.serviceInstances = append(sm.serviceInstances, newService)
|
||||
|
||||
if !sm.config.DisableServiceUpdates {
|
||||
log.Debugf("(svcs) will update [%s/%s]", newService.serviceSnapshot.Namespace, newService.serviceSnapshot.Name)
|
||||
if err := sm.updateStatus(newService); err != nil {
|
||||
// delete service to collect garbage
|
||||
if deleteErr := sm.deleteService(newService.UID); err != nil {
|
||||
return deleteErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
serviceIPs := fetchServiceAddresses(svc)
|
||||
|
||||
// Check if we need to flush any conntrack connections (due to some dangling conntrack connections)
|
||||
if svc.Annotations[flushContrack] == "true" {
|
||||
log.Debugf("Flushing conntrack rules for service [%s]", svc.Name)
|
||||
for _, serviceIP := range serviceIPs {
|
||||
err = vip.DeleteExistingSessions(serviceIP, false)
|
||||
if err != nil {
|
||||
log.Errorf("Error flushing any remaining egress connections [%s]", err)
|
||||
}
|
||||
err = vip.DeleteExistingSessions(serviceIP, true)
|
||||
if err != nil {
|
||||
log.Errorf("Error flushing any remaining ingress connections [%s]", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if egress is enabled on the service, if so we'll need to configure some rules
|
||||
if svc.Annotations[egress] == "true" && len(serviceIPs) > 0 {
|
||||
log.Debugf("Enabling egress for the service [%s]", svc.Name)
|
||||
if svc.Annotations[activeEndpoint] != "" {
|
||||
// We will need to modify the iptables rules
|
||||
err = sm.iptablesCheck()
|
||||
if err != nil {
|
||||
log.Errorf("Error configuring egress for loadbalancer [%s]", err)
|
||||
}
|
||||
errList := []error{}
|
||||
for _, serviceIP := range serviceIPs {
|
||||
podIPs := svc.Annotations[activeEndpoint]
|
||||
if sm.config.EnableEndpointSlices && vip.IsIPv6(serviceIP) {
|
||||
podIPs = svc.Annotations[activeEndpointIPv6]
|
||||
}
|
||||
err = sm.configureEgress(serviceIP, podIPs, svc.Annotations[egressDestinationPorts], svc.Namespace)
|
||||
if err != nil {
|
||||
errList = append(errList, err)
|
||||
log.Errorf("Error configuring egress for loadbalancer [%s]", err)
|
||||
}
|
||||
}
|
||||
if len(errList) == 0 {
|
||||
var provider epProvider
|
||||
if !sm.config.EnableEndpointSlices {
|
||||
provider = &endpointsProvider{label: "endpoints"}
|
||||
} else {
|
||||
provider = &endpointslicesProvider{label: "endpointslices"}
|
||||
}
|
||||
err = provider.updateServiceAnnotation(svc.Annotations[activeEndpoint], svc.Annotations[activeEndpointIPv6], svc, sm)
|
||||
if err != nil {
|
||||
log.Errorf("error configuring egress annotation for loadbalancer [%s]", err)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finishTime := time.Since(startTime)
|
||||
log.Infof("[service] synchronised in %dms", finishTime.Milliseconds())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) deleteService(uid string) error {
|
||||
// protect multiple calls
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
var updatedInstances []*Instance
|
||||
var serviceInstance *Instance
|
||||
found := false
|
||||
for x := range sm.serviceInstances {
|
||||
log.Debugf("Looking for [%s], found [%s]", uid, sm.serviceInstances[x].UID)
|
||||
// Add the running services to the new array
|
||||
if sm.serviceInstances[x].UID != uid {
|
||||
updatedInstances = append(updatedInstances, sm.serviceInstances[x])
|
||||
} else {
|
||||
// Flip the found when we match
|
||||
found = true
|
||||
serviceInstance = sm.serviceInstances[x]
|
||||
}
|
||||
}
|
||||
// If we've been through all services and not found the correct one then error
|
||||
if !found {
|
||||
// TODO: - fix UX
|
||||
// return fmt.Errorf("unable to find/stop service [%s]", uid)
|
||||
return nil
|
||||
}
|
||||
shared := false
|
||||
vipSet := make(map[string]interface{})
|
||||
for x := range updatedInstances {
|
||||
for _, vip := range updatedInstances[x].VIPs {
|
||||
vipSet[vip] = nil
|
||||
}
|
||||
}
|
||||
for _, vip := range serviceInstance.VIPs {
|
||||
if _, found := vipSet[vip]; found {
|
||||
shared = true
|
||||
}
|
||||
}
|
||||
if !shared {
|
||||
for x := range serviceInstance.clusters {
|
||||
serviceInstance.clusters[x].Stop()
|
||||
}
|
||||
if serviceInstance.isDHCP {
|
||||
serviceInstance.dhcpClient.Stop()
|
||||
macvlan, err := netlink.LinkByName(serviceInstance.dhcpInterface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding VIP Interface: %v", err)
|
||||
}
|
||||
|
||||
err = netlink.LinkDel(macvlan)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting DHCP Link : %v", err)
|
||||
}
|
||||
}
|
||||
// TODO: Implement dual-stack loadbalancer support if BGP is enabled
|
||||
for i := range serviceInstance.vipConfigs {
|
||||
if serviceInstance.vipConfigs[i].EnableBGP {
|
||||
cidrVip := fmt.Sprintf("%s/%s", serviceInstance.vipConfigs[i].VIP, serviceInstance.vipConfigs[i].VIPCIDR)
|
||||
err := sm.bgpServer.DelHost(cidrVip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[BGP] error deleting BGP host: %v", err)
|
||||
}
|
||||
log.Debugf("[BGP] deleted host: %s", cidrVip)
|
||||
}
|
||||
}
|
||||
|
||||
// We will need to tear down the egress
|
||||
if serviceInstance.serviceSnapshot.Annotations[egress] == "true" {
|
||||
if serviceInstance.serviceSnapshot.Annotations[activeEndpoint] != "" {
|
||||
log.Infof("service [%s] has an egress re-write enabled", serviceInstance.serviceSnapshot.Name)
|
||||
err := sm.TeardownEgress(serviceInstance.serviceSnapshot.Annotations[activeEndpoint], serviceInstance.serviceSnapshot.Spec.LoadBalancerIP, serviceInstance.serviceSnapshot.Annotations[egressDestinationPorts], serviceInstance.serviceSnapshot.Namespace)
|
||||
if err != nil {
|
||||
log.Errorf("%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the service array
|
||||
sm.serviceInstances = updatedInstances
|
||||
|
||||
log.Infof("Removed [%s] from manager, [%d] advertised services remain", uid, len(sm.serviceInstances))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) upnpMap(s *Instance) {
|
||||
// If upnp is enabled then update the gateway/router with the address
|
||||
// TODO - work out if we need to mapping.Reclaim()
|
||||
// TODO - check if this implementation for dualstack is correct
|
||||
if sm.upnp != nil {
|
||||
for _, vip := range s.VIPs {
|
||||
log.Infof("[UPNP] Adding map to [%s:%d - %s]", vip, s.Port, s.serviceSnapshot.Name)
|
||||
if err := sm.upnp.AddPortMapping(int(s.Port), int(s.Port), 0, vip, strings.ToUpper(s.Type), s.serviceSnapshot.Name); err == nil {
|
||||
log.Infof("service should be accessible externally on port [%d]", s.Port)
|
||||
} else {
|
||||
sm.upnp.Reclaim()
|
||||
log.Errorf("unable to map port to gateway [%s]", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *Manager) updateStatus(i *Instance) error {
|
||||
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
// Retrieve the latest version of Deployment before attempting update
|
||||
// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
|
||||
currentService, err := sm.clientSet.CoreV1().Services(i.serviceSnapshot.Namespace).Get(context.TODO(), i.serviceSnapshot.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentServiceCopy := currentService.DeepCopy()
|
||||
if currentServiceCopy.Annotations == nil {
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
// If we're using ARP then we can only broadcast the VIP from one place, add an annotation to the service
|
||||
if sm.config.EnableARP {
|
||||
// Add the current host
|
||||
currentServiceCopy.Annotations[vipHost] = id
|
||||
}
|
||||
if i.dhcpInterfaceHwaddr != "" || i.dhcpInterfaceIP != "" {
|
||||
currentServiceCopy.Annotations[hwAddrKey] = i.dhcpInterfaceHwaddr
|
||||
currentServiceCopy.Annotations[requestedIP] = i.dhcpInterfaceIP
|
||||
}
|
||||
|
||||
if !cmp.Equal(currentService, currentServiceCopy) {
|
||||
currentService, err = sm.clientSet.CoreV1().Services(currentServiceCopy.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("Error updating Service Spec [%s] : %v", i.serviceSnapshot.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ports := make([]v1.PortStatus, 0, len(i.serviceSnapshot.Spec.Ports))
|
||||
for _, port := range i.serviceSnapshot.Spec.Ports {
|
||||
ports = append(ports, v1.PortStatus{
|
||||
Port: port.Port,
|
||||
Protocol: port.Protocol,
|
||||
})
|
||||
}
|
||||
|
||||
ingresses := []v1.LoadBalancerIngress{}
|
||||
|
||||
for _, c := range i.vipConfigs {
|
||||
if !vip.IsIP(c.VIP) {
|
||||
ips, err := vip.LookupHost(c.VIP, sm.config.DNSMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ip := range ips {
|
||||
i := v1.LoadBalancerIngress{
|
||||
IP: ip,
|
||||
Ports: ports,
|
||||
}
|
||||
ingresses = append(ingresses, i)
|
||||
}
|
||||
} else {
|
||||
i := v1.LoadBalancerIngress{
|
||||
IP: c.VIP,
|
||||
Ports: ports,
|
||||
}
|
||||
ingresses = append(ingresses, i)
|
||||
}
|
||||
}
|
||||
if !cmp.Equal(currentService.Status.LoadBalancer.Ingress, ingresses) {
|
||||
currentService.Status.LoadBalancer.Ingress = ingresses
|
||||
_, err = sm.clientSet.CoreV1().Services(currentService.Namespace).UpdateStatus(context.TODO(), currentService, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("Error updating Service %s/%s Status: %v", i.serviceSnapshot.Namespace, i.serviceSnapshot.Name, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if retryErr != nil {
|
||||
log.Errorf("Failed to set Services: %v", retryErr)
|
||||
return retryErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchServiceAddresses tries to get the addresses from annotations
|
||||
// kube-vip.io/loadbalancerIPs, then from spec.loadbalancerIP
|
||||
func fetchServiceAddresses(s *v1.Service) []string {
|
||||
annotationAvailable := false
|
||||
if s.Annotations != nil {
|
||||
if v, annotationAvailable := s.Annotations[loadbalancerIPAnnotation]; annotationAvailable {
|
||||
ips := strings.Split(v, ",")
|
||||
var trimmedIPs []string
|
||||
for _, ip := range ips {
|
||||
trimmedIPs = append(trimmedIPs, strings.TrimSpace(ip))
|
||||
}
|
||||
return trimmedIPs
|
||||
}
|
||||
}
|
||||
|
||||
if !annotationAvailable {
|
||||
if len(s.Status.LoadBalancer.Ingress) > 0 {
|
||||
addresses := []string{}
|
||||
for _, ingress := range s.Status.LoadBalancer.Ingress {
|
||||
addresses = append(addresses, ingress.IP)
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
}
|
||||
|
||||
if s.Spec.LoadBalancerIP != "" {
|
||||
return []string{s.Spec.LoadBalancerIP}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
)
|
||||
|
||||
// The startServicesWatchForLeaderElection function will start a services watcher, the
|
||||
func (sm *Manager) startServicesWatchForLeaderElection(ctx context.Context) error {
|
||||
|
||||
err := sm.servicesWatcher(ctx, sm.StartServicesLeaderElection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.clusters {
|
||||
for i := range cluster.Network {
|
||||
_ = cluster.Network[i].DeleteRoute()
|
||||
}
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Shutting down kube-Vip")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// The startServicesWatchForLeaderElection function will start a services watcher, the
|
||||
func (sm *Manager) StartServicesLeaderElection(ctx context.Context, service *v1.Service, wg *sync.WaitGroup) error {
|
||||
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceLease := fmt.Sprintf("kubevip-%s", service.Name)
|
||||
log.Infof("(svc election) service [%s], namespace [%s], lock name [%s], host id [%s]", service.Name, service.Namespace, serviceLease, id)
|
||||
// we use the Lease lock type since edits to Leases are less common
|
||||
// and fewer objects in the cluster watch "all Leases".
|
||||
lock := &resourcelock.LeaseLock{
|
||||
LeaseMeta: metav1.ObjectMeta{
|
||||
Name: serviceLease,
|
||||
Namespace: service.Namespace,
|
||||
},
|
||||
Client: sm.clientSet.CoordinationV1(),
|
||||
LockConfig: resourcelock.ResourceLockConfig{
|
||||
Identity: id,
|
||||
},
|
||||
}
|
||||
|
||||
activeService[string(service.UID)] = true
|
||||
// start the leader election code loop
|
||||
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
|
||||
Lock: lock,
|
||||
// IMPORTANT: you MUST ensure that any code you have that
|
||||
// is protected by the lease must terminate **before**
|
||||
// you call cancel. Otherwise, you could have a background
|
||||
// loop still running and another process could
|
||||
// get elected before your background loop finished, violating
|
||||
// the stated goal of the lease.
|
||||
ReleaseOnCancel: true,
|
||||
LeaseDuration: time.Duration(sm.config.LeaseDuration) * time.Second,
|
||||
RenewDeadline: time.Duration(sm.config.RenewDeadline) * time.Second,
|
||||
RetryPeriod: time.Duration(sm.config.RetryPeriod) * time.Second,
|
||||
Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
// Mark this service as active (as we've started leading)
|
||||
// we run this in background as it's blocking
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := sm.syncServices(ctx, service, wg); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}()
|
||||
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
// we can do cleanup here
|
||||
log.Infof("(svc election) service [%s] leader lost: [%s]", service.Name, id)
|
||||
if activeService[string(service.UID)] {
|
||||
if err := sm.deleteService(string(service.UID)); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
// Mark this service is inactive
|
||||
activeService[string(service.UID)] = false
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
if identity == id {
|
||||
// I just got the lock
|
||||
return
|
||||
}
|
||||
log.Infof("(svc election) new leader elected: %s", identity)
|
||||
},
|
||||
},
|
||||
})
|
||||
log.Infof("(svc election) for service [%s] stopping", service.Name)
|
||||
return nil
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -27,13 +27,9 @@ import (
|
||||
// present
|
||||
func (sm *Manager) annotationsWatcher() error {
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
log.Infof("Kube-Vip is waiting for annotation prefix [%s] to be present on this node", sm.config.Annotations)
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("Kube-Vip is waiting for annotation prefix to be present on this node", "prefix", sm.config.Annotations)
|
||||
|
||||
labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{"kubernetes.io/hostname": hostname}}
|
||||
labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{"kubernetes.io/hostname": sm.config.NodeName}}
|
||||
listOptions := metav1.ListOptions{
|
||||
LabelSelector: labels.Set(labelSelector.MatchLabels).String(),
|
||||
}
|
||||
@@ -59,11 +55,12 @@ func (sm *Manager) annotationsWatcher() error {
|
||||
|
||||
// We got an error with the annotations, falling back to the watch until
|
||||
// they're as needed
|
||||
log.Warn(err)
|
||||
log.Warn(err.Error())
|
||||
|
||||
rw, err := watchtools.NewRetryWatcher(node.ResourceVersion, &cache.ListWatch{
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.clientSet.CoreV1().Nodes().Watch(context.Background(), listOptions)
|
||||
// TODO, will need refactoring as part of rikatz work
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(context.TODO(), node.ResourceVersion, &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.rwClientSet.CoreV1().Nodes().Watch(context.Background(), listOptions)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -99,7 +96,7 @@ func (sm *Manager) annotationsWatcher() error {
|
||||
|
||||
bgpConfig, bgpPeer, err := parseBgpAnnotations(sm.config.BGPConfig, node, sm.config.Annotations)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -113,7 +110,7 @@ func (sm *Manager) annotationsWatcher() error {
|
||||
return fmt.Errorf("unable to parse Kubernetes Node from Kubernetes watcher")
|
||||
}
|
||||
|
||||
log.Infof("Node [%s] has been deleted", node.Name)
|
||||
log.Info("Node has been deleted", "name", node.Name)
|
||||
|
||||
case watch.Bookmark:
|
||||
// Un-used
|
||||
@@ -124,17 +121,17 @@ func (sm *Manager) annotationsWatcher() error {
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, ok := errObject.(*apierrors.StatusError)
|
||||
if !ok {
|
||||
log.Errorf(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
|
||||
log.Error(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
|
||||
|
||||
}
|
||||
|
||||
status := statusErr.ErrStatus
|
||||
log.Errorf("%v", status)
|
||||
log.Error(status.String())
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(exitFunction)
|
||||
log.Infoln("Exiting Annotations watcher")
|
||||
log.Info("Exiting Annotations watcher")
|
||||
return nil
|
||||
|
||||
}
|
||||
@@ -154,8 +151,8 @@ func (sm *Manager) annotationsWatcher() error {
|
||||
// * `<info>` is the relevant information, such as `node-asn` or `peer-ip`
|
||||
// * `{{n}}` is the number of the peer, always starting with `0`
|
||||
// * kube-vip is only designed to manage one peer, just look for {{n}} == 0
|
||||
func parseBgpAnnotations(bgpConfig bgp.Config, node *v1.Node, prefix string) (bgp.Config, bgp.Peer, error) {
|
||||
bgpPeer := bgp.Peer{}
|
||||
func parseBgpAnnotations(bgpConfig kubevip.BGPConfig, node *v1.Node, prefix string) (kubevip.BGPConfig, kubevip.BGPPeer, error) {
|
||||
bgpPeer := kubevip.BGPPeer{}
|
||||
|
||||
nodeASN := ""
|
||||
for k, v := range node.Annotations {
|
||||
@@ -219,7 +216,7 @@ func parseBgpAnnotations(bgpConfig bgp.Config, node *v1.Node, prefix string) (bg
|
||||
|
||||
peerIPs := strings.Split(peerIPString, ",")
|
||||
|
||||
bgpConfig.Peers = make([]bgp.Peer, 0, len(peerIPs))
|
||||
bgpConfig.Peers = make([]kubevip.BGPPeer, 0, len(peerIPs))
|
||||
for _, peerIP := range peerIPs {
|
||||
ipAddr := strings.TrimSpace(peerIP)
|
||||
|
||||
|
||||
@@ -1,493 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
"k8s.io/client-go/util/retry"
|
||||
)
|
||||
|
||||
type epProvider interface {
|
||||
createRetryWatcher(context.Context, *Manager,
|
||||
*v1.Service) (*watchtools.RetryWatcher, error)
|
||||
getAllEndpoints() ([]string, error)
|
||||
getLocalEndpoints(string, *kubevip.Config) ([]string, error)
|
||||
getLabel() string
|
||||
updateServiceAnnotation(string, string, *v1.Service, *Manager) error
|
||||
loadObject(runtime.Object, context.CancelFunc) error
|
||||
getProtocol() string
|
||||
}
|
||||
|
||||
type endpointsProvider struct {
|
||||
label string
|
||||
endpoints *v1.Endpoints
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) createRetryWatcher(ctx context.Context, sm *Manager,
|
||||
service *v1.Service) (*watchtools.RetryWatcher, error) {
|
||||
opts := metav1.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector("metadata.name", service.Name).String(),
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcher("1", &cache.ListWatch{
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.clientSet.CoreV1().Endpoints(service.Namespace).Watch(ctx, opts)
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating endpoint watcher: %s", err.Error())
|
||||
}
|
||||
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) loadObject(endpoints runtime.Object, cancel context.CancelFunc) error {
|
||||
eps, ok := endpoints.(*v1.Endpoints)
|
||||
if !ok {
|
||||
cancel()
|
||||
return fmt.Errorf("[%s] unable to parse Kubernetes services from API watcher", ep.getLabel())
|
||||
}
|
||||
ep.endpoints = eps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) getAllEndpoints() ([]string, error) {
|
||||
result := []string{}
|
||||
for subset := range ep.endpoints.Subsets {
|
||||
for address := range ep.endpoints.Subsets[subset].Addresses {
|
||||
addr := strings.Split(ep.endpoints.Subsets[subset].Addresses[address].IP, "/")
|
||||
result = append(result, addr[0])
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) getLocalEndpoints(id string, config *kubevip.Config) ([]string, error) {
|
||||
var localEndpoints []string
|
||||
|
||||
shortname, shortnameErr := getShortname(id)
|
||||
if shortnameErr != nil {
|
||||
if config.EnableRoutingTable && (!config.EnableLeaderElection && !config.EnableServicesElection) {
|
||||
log.Debugf("[%s] %v, shortname will not be used", ep.label, shortnameErr)
|
||||
} else {
|
||||
log.Errorf("[%s] %v", ep.label, shortnameErr)
|
||||
}
|
||||
}
|
||||
|
||||
for subset := range ep.endpoints.Subsets {
|
||||
for address := range ep.endpoints.Subsets[subset].Addresses {
|
||||
// 1. Compare the hostname on the endpoint to the hostname
|
||||
// 2. Compare the nodename on the endpoint to the hostname
|
||||
// 3. Drop the FQDN to a shortname and compare to the nodename on the endpoint
|
||||
|
||||
// 1. Compare the Hostname first (should be FQDN)
|
||||
log.Debugf("[%s] processing endpoint [%s]", ep.label, ep.endpoints.Subsets[subset].Addresses[address].IP)
|
||||
if id == ep.endpoints.Subsets[subset].Addresses[address].Hostname {
|
||||
log.Debugf("[%s] found local endpoint - address: %s, hostname: %s",
|
||||
ep.label, ep.endpoints.Subsets[subset].Addresses[address].IP, ep.endpoints.Subsets[subset].Addresses[address].Hostname)
|
||||
localEndpoints = append(localEndpoints, ep.endpoints.Subsets[subset].Addresses[address].IP)
|
||||
} else {
|
||||
// 2. Compare the Nodename (from testing could be FQDN or short)
|
||||
if ep.endpoints.Subsets[subset].Addresses[address].NodeName != nil {
|
||||
if id == *ep.endpoints.Subsets[subset].Addresses[address].NodeName {
|
||||
log.Debugf("[%s] found local endpoint - address: %s, hostname: %s, node: %s",
|
||||
ep.label, ep.endpoints.Subsets[subset].Addresses[address].IP, ep.endpoints.Subsets[subset].Addresses[address].Hostname,
|
||||
*ep.endpoints.Subsets[subset].Addresses[address].NodeName)
|
||||
localEndpoints = append(localEndpoints, ep.endpoints.Subsets[subset].Addresses[address].IP)
|
||||
} else if shortnameErr == nil && shortname == *ep.endpoints.Subsets[subset].Addresses[address].NodeName {
|
||||
log.Debugf("[%s] found local endpoint - address: %s, shortname: %s, node: %s",
|
||||
ep.label, ep.endpoints.Subsets[subset].Addresses[address].IP, shortname, *ep.endpoints.Subsets[subset].Addresses[address].NodeName)
|
||||
localEndpoints = append(localEndpoints, ep.endpoints.Subsets[subset].Addresses[address].IP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return localEndpoints, nil
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) updateServiceAnnotation(endpoint string, _ string, service *v1.Service, sm *Manager) error {
|
||||
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
// Retrieve the latest version of Deployment before attempting update
|
||||
// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
|
||||
currentService, err := sm.clientSet.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentServiceCopy := currentService.DeepCopy()
|
||||
if currentServiceCopy.Annotations == nil {
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
currentServiceCopy.Annotations[activeEndpoint] = endpoint
|
||||
|
||||
_, err = sm.clientSet.CoreV1().Services(currentService.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("[%s] error updating Service Spec [%s] : %v", ep.getLabel(), currentServiceCopy.Name, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if retryErr != nil {
|
||||
log.Errorf("[%s] failed to set Services: %v", ep.getLabel(), retryErr)
|
||||
return retryErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) getLabel() string {
|
||||
return ep.label
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) getProtocol() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sm *Manager) watchEndpoint(ctx context.Context, id string, service *v1.Service, wg *sync.WaitGroup, provider epProvider) error {
|
||||
log.Infof("[%s] watching for service [%s] in namespace [%s]", provider.getLabel(), service.Name, service.Namespace)
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
leaderContext, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var leaderElectionActive bool
|
||||
|
||||
rw, err := provider.createRetryWatcher(leaderContext, sm, service)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return fmt.Errorf("[%s] error watching endpoints: %w", provider.getLabel(), err)
|
||||
}
|
||||
|
||||
exitFunction := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debugf("[%s] context cancelled", provider.getLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
case <-sm.shutdownChan:
|
||||
log.Debugf("[%s] shutdown called", provider.getLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
case <-exitFunction:
|
||||
log.Debugf("[%s] function ending", provider.getLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ch := rw.ResultChan()
|
||||
|
||||
var lastKnownGoodEndpoint string
|
||||
for event := range ch {
|
||||
activeEndpointAnnotation := activeEndpoint
|
||||
// We need to inspect the event and get ResourceVersion out of it
|
||||
switch event.Type {
|
||||
|
||||
case watch.Added, watch.Modified:
|
||||
|
||||
if err = provider.loadObject(event.Object, cancel); err != nil {
|
||||
return fmt.Errorf("[%s] error loading k8s object: %w", provider.getLabel(), err)
|
||||
}
|
||||
|
||||
if sm.config.EnableEndpointSlices && provider.getProtocol() == string(discoveryv1.AddressTypeIPv6) {
|
||||
activeEndpointAnnotation = activeEndpointIPv6
|
||||
}
|
||||
|
||||
// Build endpoints
|
||||
var endpoints []string
|
||||
if (sm.config.EnableBGP || sm.config.EnableRoutingTable) && !sm.config.EnableLeaderElection && !sm.config.EnableServicesElection &&
|
||||
service.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
if endpoints, err = provider.getAllEndpoints(); err != nil {
|
||||
return fmt.Errorf("[%s] error getting all endpoints: %w", provider.getLabel(), err)
|
||||
}
|
||||
} else {
|
||||
if endpoints, err = provider.getLocalEndpoints(id, sm.config); err != nil {
|
||||
return fmt.Errorf("[%s] error getting local endpoints: %w", provider.getLabel(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Find out if we have any local endpoints
|
||||
// if out endpoint is empty then populate it
|
||||
// if not, go through the endpoints and see if ours still exists
|
||||
// If we have a local endpoint then begin the leader Election, unless it's already running
|
||||
//
|
||||
|
||||
// Check that we have local endpoints
|
||||
if len(endpoints) != 0 {
|
||||
// if we haven't populated one, then do so
|
||||
if lastKnownGoodEndpoint != "" {
|
||||
|
||||
// check out previous endpoint exists
|
||||
stillExists := false
|
||||
|
||||
for x := range endpoints {
|
||||
if endpoints[x] == lastKnownGoodEndpoint {
|
||||
stillExists = true
|
||||
}
|
||||
}
|
||||
// If the last endpoint no longer exists, we cancel our leader Election
|
||||
if !stillExists && leaderElectionActive {
|
||||
if sm.config.EnableServicesElection || sm.config.EnableLeaderElection {
|
||||
log.Warnf("[%s] existing [%s] has been removed, restarting leaderElection", provider.getLabel(), lastKnownGoodEndpoint)
|
||||
// Stop the existing leaderElection
|
||||
cancel()
|
||||
}
|
||||
// Set our active endpoint to an existing one
|
||||
lastKnownGoodEndpoint = endpoints[0]
|
||||
// disable last leaderElection flag
|
||||
leaderElectionActive = false
|
||||
}
|
||||
|
||||
} else {
|
||||
lastKnownGoodEndpoint = endpoints[0]
|
||||
}
|
||||
|
||||
// Set the service accordingly
|
||||
if service.Annotations[egress] == "true" {
|
||||
service.Annotations[activeEndpointAnnotation] = lastKnownGoodEndpoint
|
||||
}
|
||||
|
||||
if !leaderElectionActive && sm.config.EnableServicesElection {
|
||||
go func() {
|
||||
leaderContext, cancel = context.WithCancel(context.Background())
|
||||
|
||||
// This is a blocking function, that will restart (in the event of failure)
|
||||
for {
|
||||
// if the context isn't cancelled restart
|
||||
if leaderContext.Err() != context.Canceled {
|
||||
leaderElectionActive = true
|
||||
err := sm.StartServicesLeaderElection(leaderContext, service, wg)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
leaderElectionActive = false
|
||||
} else {
|
||||
leaderElectionActive = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
isRouteConfigured, err := isRouteConfigured(service.UID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error while checking if route is configured: %w", provider.getLabel(), err)
|
||||
}
|
||||
// There are local endpoints available on the node
|
||||
if !sm.config.EnableServicesElection && !sm.config.EnableLeaderElection && !isRouteConfigured {
|
||||
// If routing table mode is enabled - routes should be added per node
|
||||
if sm.config.EnableRoutingTable {
|
||||
if instance := sm.findServiceInstance(service); instance != nil {
|
||||
for _, cluster := range instance.clusters {
|
||||
for i := range cluster.Network {
|
||||
err := cluster.Network[i].AddRoute()
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EEXIST) {
|
||||
// If route exists try to update it if necessary
|
||||
isUpdated, err := cluster.Network[i].UpdateRoutes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error updating existing routes: %w", provider.getLabel(), err)
|
||||
}
|
||||
if isUpdated {
|
||||
log.Debugf("[%s] updated route: %s", provider.getLabel(), cluster.Network[i].IP())
|
||||
}
|
||||
} else {
|
||||
// If other error occurs, return error
|
||||
return fmt.Errorf("[%s] error adding route: %s", provider.getLabel(), err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Infof("[%s] added route: %s, service: %s/%s, interface: %s, table: %d",
|
||||
provider.getLabel(), cluster.Network[i].IP(), service.Namespace, service.Name, cluster.Network[i].Interface(), sm.config.RoutingTableID)
|
||||
configuredLocalRoutes.Store(string(service.UID), true)
|
||||
leaderElectionActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If BGP mode is enabled - hosts should be added per node
|
||||
if sm.config.EnableBGP {
|
||||
if instance := sm.findServiceInstance(service); instance != nil {
|
||||
for _, cluster := range instance.clusters {
|
||||
for i := range cluster.Network {
|
||||
address := fmt.Sprintf("%s/%s", cluster.Network[i].IP(), sm.config.VIPCIDR)
|
||||
log.Debugf("[%s] attempting to advertise BGP service: %s", provider.getLabel(), address)
|
||||
err := sm.bgpServer.AddHost(address)
|
||||
if err != nil {
|
||||
log.Errorf("[%s] error adding BGP host %s\n", err.Error(), provider.getLabel())
|
||||
} else {
|
||||
log.Infof("[%s] added BGP host: %s, service: %s/%s",
|
||||
provider.getLabel(), address, service.Namespace, service.Name)
|
||||
configuredLocalRoutes.Store(string(service.UID), true)
|
||||
leaderElectionActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There are no local enpoints
|
||||
if !sm.config.EnableServicesElection && !sm.config.EnableLeaderElection {
|
||||
// If routing table mode is enabled - routes should be deleted
|
||||
if sm.config.EnableRoutingTable {
|
||||
if errs := sm.clearRoutes(service); len(errs) == 0 {
|
||||
configuredLocalRoutes.Store(string(service.UID), false)
|
||||
}
|
||||
}
|
||||
|
||||
// If BGP mode is enabled - routes should be deleted
|
||||
if sm.config.EnableBGP {
|
||||
if instance := sm.findServiceInstance(service); instance != nil {
|
||||
for _, cluster := range instance.clusters {
|
||||
for i := range cluster.Network {
|
||||
address := fmt.Sprintf("%s/%s", cluster.Network[i].IP(), sm.config.VIPCIDR)
|
||||
err := sm.bgpServer.DelHost(address)
|
||||
if err != nil {
|
||||
log.Errorf("[%s] error deleting BGP host%s: %s\n", provider.getLabel(), address, err.Error())
|
||||
} else {
|
||||
log.Infof("[%s] deleted BGP host: %s, service: %s/%s",
|
||||
provider.getLabel(), address, service.Namespace, service.Name)
|
||||
configuredLocalRoutes.Store(string(service.UID), false)
|
||||
leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no local endpoints, and we had one then remove it and stop the leaderElection
|
||||
if lastKnownGoodEndpoint != "" {
|
||||
log.Warnf("[%s] existing [%s] has been removed, no remaining endpoints for leaderElection", provider.getLabel(), lastKnownGoodEndpoint)
|
||||
lastKnownGoodEndpoint = "" // reset endpoint
|
||||
if sm.config.EnableServicesElection || sm.config.EnableLeaderElection {
|
||||
cancel() // stop services watcher
|
||||
}
|
||||
leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
log.Debugf("[%s watcher] service %s/%s: local endpoint(s) [%d], known good [%s], active election [%t]",
|
||||
provider.getLabel(), service.Namespace, service.Name, len(endpoints), lastKnownGoodEndpoint, leaderElectionActive)
|
||||
|
||||
case watch.Deleted:
|
||||
// When no-leader-elecition mode
|
||||
if !sm.config.EnableServicesElection && !sm.config.EnableLeaderElection {
|
||||
// find all existing local endpoints
|
||||
var endpoints []string
|
||||
if (sm.config.EnableBGP || sm.config.EnableRoutingTable) && !sm.config.EnableLeaderElection && !sm.config.EnableServicesElection &&
|
||||
service.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
if endpoints, err = provider.getAllEndpoints(); err != nil {
|
||||
return fmt.Errorf("[%s] error getting all endpoints: %w", provider.getLabel(), err)
|
||||
}
|
||||
} else {
|
||||
if endpoints, err = provider.getLocalEndpoints(id, sm.config); err != nil {
|
||||
return fmt.Errorf("[%s] error getting all endpoints: %w", provider.getLabel(), err)
|
||||
}
|
||||
}
|
||||
|
||||
// If there were local endpoints deleted
|
||||
if len(endpoints) > 0 {
|
||||
// Delete all routes in routing table mode
|
||||
if sm.config.EnableRoutingTable {
|
||||
sm.clearRoutes(service)
|
||||
}
|
||||
|
||||
// Delete all hosts in BGP mode
|
||||
if sm.config.EnableBGP {
|
||||
sm.clearBGPHosts(service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the goroutine that will end the retry watcher, then exit the endpoint watcher function
|
||||
close(exitFunction)
|
||||
log.Infof("[%s] deleted stopping watching for [%s] in namespace [%s]", provider.getLabel(), service.Name, service.Namespace)
|
||||
|
||||
return nil
|
||||
case watch.Error:
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, _ := errObject.(*apierrors.StatusError)
|
||||
log.Errorf("[%s] -> %v", provider.getLabel(), statusErr)
|
||||
}
|
||||
}
|
||||
close(exitFunction)
|
||||
log.Infof("[%s] stopping watching for [%s] in namespace [%s]", provider.getLabel(), service.Name, service.Namespace)
|
||||
return nil //nolint:govet
|
||||
}
|
||||
|
||||
func (sm *Manager) clearRoutes(service *v1.Service) []error {
|
||||
errs := []error{}
|
||||
if instance := sm.findServiceInstance(service); instance != nil {
|
||||
for _, cluster := range instance.clusters {
|
||||
for i := range cluster.Network {
|
||||
err := cluster.Network[i].DeleteRoute()
|
||||
if err != nil && !errors.Is(err, syscall.ESRCH) {
|
||||
log.Errorf("failed to delete route for %s: %s", cluster.Network[i].IP(), err.Error())
|
||||
errs = append(errs, err)
|
||||
}
|
||||
log.Debugf("deleted route: %s, service: %s/%s, interface: %s, table: %d",
|
||||
cluster.Network[i].IP(), service.Namespace, service.Name, cluster.Network[i].Interface(), sm.config.RoutingTableID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (sm *Manager) clearBGPHosts(service *v1.Service) {
|
||||
if instance := sm.findServiceInstance(service); instance != nil {
|
||||
for _, cluster := range instance.clusters {
|
||||
for i := range cluster.Network {
|
||||
address := fmt.Sprintf("%s/%s", cluster.Network[i].IP(), sm.config.VIPCIDR)
|
||||
err := sm.bgpServer.DelHost(address)
|
||||
if err != nil {
|
||||
log.Errorf("[endpoint] error deleting BGP host %s\n", err.Error())
|
||||
} else {
|
||||
log.Debugf("[endpoint] deleted BGP host: %s, service: %s/%s",
|
||||
address, service.Namespace, service.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns just the shortname (or first bit) of a FQDN
|
||||
func getShortname(hostname string) (string, error) {
|
||||
if len(hostname) == 0 {
|
||||
return "", fmt.Errorf("unable to find shortname from %s", hostname)
|
||||
}
|
||||
hostParts := strings.Split(hostname, ".")
|
||||
if len(hostParts) >= 1 {
|
||||
return hostParts[0], nil
|
||||
}
|
||||
return "", fmt.Errorf("unable to find shortname from %s", hostname)
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
"k8s.io/client-go/util/retry"
|
||||
)
|
||||
|
||||
type endpointslicesProvider struct {
|
||||
label string
|
||||
endpoints *discoveryv1.EndpointSlice
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) createRetryWatcher(ctx context.Context, sm *Manager,
|
||||
service *v1.Service) (*watchtools.RetryWatcher, error) {
|
||||
labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{"kubernetes.io/service-name": service.Name}}
|
||||
|
||||
opts := metav1.ListOptions{
|
||||
LabelSelector: labels.Set(labelSelector.MatchLabels).String(),
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcher("1", &cache.ListWatch{
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.clientSet.DiscoveryV1().EndpointSlices(service.Namespace).Watch(ctx, opts)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[%s] error creating endpointslices watcher: %s", ep.label, err.Error())
|
||||
}
|
||||
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) loadObject(endpoints runtime.Object, cancel context.CancelFunc) error {
|
||||
eps, ok := endpoints.(*discoveryv1.EndpointSlice)
|
||||
if !ok {
|
||||
cancel()
|
||||
return fmt.Errorf("[%s] error casting endpoints to v1.Endpoints struct", ep.label)
|
||||
}
|
||||
ep.endpoints = eps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getAllEndpoints() ([]string, error) {
|
||||
result := []string{}
|
||||
for _, ep := range ep.endpoints.Endpoints {
|
||||
result = append(result, ep.Addresses...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getLocalEndpoints(id string, config *kubevip.Config) ([]string, error) {
|
||||
shortname, shortnameErr := getShortname(id)
|
||||
if shortnameErr != nil {
|
||||
if config.EnableRoutingTable && (!config.EnableLeaderElection && !config.EnableServicesElection) {
|
||||
log.Debugf("[%s] %v, shortname will not be used", ep.label, shortnameErr)
|
||||
} else {
|
||||
log.Errorf("[%s] %v", ep.label, shortnameErr)
|
||||
}
|
||||
}
|
||||
|
||||
var localEndpoints []string
|
||||
for i := range ep.endpoints.Endpoints {
|
||||
for j := range ep.endpoints.Endpoints[i].Addresses {
|
||||
// 1. Compare the hostname on the endpoint to the hostname
|
||||
// 2. Compare the nodename on the endpoint to the hostname
|
||||
// 3. Drop the FQDN to a shortname and compare to the nodename on the endpoint
|
||||
|
||||
// 1. Compare the Hostname first (should be FQDN)
|
||||
log.Debugf("[%s] processing endpoint [%s]", ep.label, ep.endpoints.Endpoints[i].Addresses[j])
|
||||
if ep.endpoints.Endpoints[i].Hostname != nil && id == *ep.endpoints.Endpoints[i].Hostname {
|
||||
if *ep.endpoints.Endpoints[i].Conditions.Serving {
|
||||
log.Debugf("[%s] found endpoint - address: %s, hostname: %s", ep.label, ep.endpoints.Endpoints[i].Addresses[j], *ep.endpoints.Endpoints[i].Hostname)
|
||||
localEndpoints = append(localEndpoints, ep.endpoints.Endpoints[i].Addresses[j])
|
||||
}
|
||||
} else {
|
||||
// 2. Compare the Nodename (from testing could be FQDN or short)
|
||||
if ep.endpoints.Endpoints[i].NodeName != nil {
|
||||
if id == *ep.endpoints.Endpoints[i].NodeName && *ep.endpoints.Endpoints[i].Conditions.Serving {
|
||||
if ep.endpoints.Endpoints[i].Hostname != nil {
|
||||
log.Debugf("[%s] found endpoint - address: %s, hostname: %s, node: %s", ep.label, ep.endpoints.Endpoints[i].Addresses[j], *ep.endpoints.Endpoints[i].Hostname, *ep.endpoints.Endpoints[i].NodeName)
|
||||
} else {
|
||||
log.Debugf("[%s] found endpoint - address: %s, node: %s", ep.label, ep.endpoints.Endpoints[i].Addresses[j], *ep.endpoints.Endpoints[i].NodeName)
|
||||
}
|
||||
localEndpoints = append(localEndpoints, ep.endpoints.Endpoints[i].Addresses[j])
|
||||
// 3. Compare to shortname
|
||||
} else if shortnameErr != nil && shortname == *ep.endpoints.Endpoints[i].NodeName && *ep.endpoints.Endpoints[i].Conditions.Serving {
|
||||
log.Debugf("[%s] found endpoint - address: %s, shortname: %s, node: %s", ep.label, ep.endpoints.Endpoints[i].Addresses[j], shortname, *ep.endpoints.Endpoints[i].NodeName)
|
||||
localEndpoints = append(localEndpoints, ep.endpoints.Endpoints[i].Addresses[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return localEndpoints, nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) updateServiceAnnotation(endpoint, endpointIPv6 string, service *v1.Service, sm *Manager) error {
|
||||
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
// Retrieve the latest version of Deployment before attempting update
|
||||
// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
|
||||
currentService, err := sm.clientSet.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentServiceCopy := currentService.DeepCopy()
|
||||
if currentServiceCopy.Annotations == nil {
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
currentServiceCopy.Annotations[activeEndpoint] = endpoint
|
||||
currentServiceCopy.Annotations[activeEndpointIPv6] = endpointIPv6
|
||||
|
||||
_, err = sm.clientSet.CoreV1().Services(currentService.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("[%s] error updating Service Spec [%s] : %v", ep.label, currentServiceCopy.Name, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if retryErr != nil {
|
||||
log.Errorf("[%s] failed to set Services: %v", ep.label, retryErr)
|
||||
return retryErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getLabel() string {
|
||||
return ep.label
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getProtocol() string {
|
||||
return string(ep.endpoints.AddressType)
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
// TODO: Fix the naming of these contexts
|
||||
|
||||
// activeServiceLoadBalancer keeps track of services that already have a leaderElection in place
|
||||
var activeServiceLoadBalancer map[string]context.Context
|
||||
|
||||
// activeServiceLoadBalancer keeps track of services that already have a leaderElection in place
|
||||
var activeServiceLoadBalancerCancel map[string]func()
|
||||
|
||||
// activeService keeps track of services that already have a leaderElection in place
|
||||
var activeService map[string]bool
|
||||
|
||||
// watchedService keeps track of services that are already being watched
|
||||
var watchedService map[string]bool
|
||||
|
||||
// watchedService keeps track of routes that has been configured on the node
|
||||
var configuredLocalRoutes sync.Map
|
||||
|
||||
func init() {
|
||||
// Set up the caches for monitoring existing active or watched services
|
||||
activeServiceLoadBalancerCancel = make(map[string]func())
|
||||
activeServiceLoadBalancer = make(map[string]context.Context)
|
||||
activeService = make(map[string]bool)
|
||||
watchedService = make(map[string]bool)
|
||||
}
|
||||
|
||||
// This function handles the watching of a services endpoints and updates a load balancers endpoint configurations accordingly
|
||||
func (sm *Manager) servicesWatcher(ctx context.Context, serviceFunc func(context.Context, *v1.Service, *sync.WaitGroup) error) error {
|
||||
// Watch function
|
||||
var wg sync.WaitGroup
|
||||
|
||||
id, err := os.Hostname()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sm.config.ServiceNamespace == "" {
|
||||
// v1.NamespaceAll is actually "", but we'll stay with the const in case things change upstream
|
||||
sm.config.ServiceNamespace = v1.NamespaceAll
|
||||
log.Infof("(svcs) starting services watcher for all namespaces")
|
||||
} else {
|
||||
log.Infof("(svcs) starting services watcher for services in namespace [%s]", sm.config.ServiceNamespace)
|
||||
}
|
||||
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
rw, err := watchtools.NewRetryWatcher("1", &cache.ListWatch{
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.clientSet.CoreV1().Services(sm.config.ServiceNamespace).Watch(ctx, metav1.ListOptions{})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating services watcher: %s", err.Error())
|
||||
}
|
||||
exitFunction := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-sm.shutdownChan:
|
||||
log.Debug("(svcs) shutdown called")
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
return
|
||||
case <-exitFunction:
|
||||
log.Debug("(svcs) function ending")
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
return
|
||||
}
|
||||
}()
|
||||
ch := rw.ResultChan()
|
||||
|
||||
// Used for tracking an active endpoint / pod
|
||||
for event := range ch {
|
||||
sm.countServiceWatchEvent.With(prometheus.Labels{"type": string(event.Type)}).Add(1)
|
||||
|
||||
// We need to inspect the event and get ResourceVersion out of it
|
||||
switch event.Type {
|
||||
case watch.Added, watch.Modified:
|
||||
// log.Debugf("Endpoints for service [%s] have been Created or modified", s.service.ServiceName)
|
||||
svc, ok := event.Object.(*v1.Service)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to parse Kubernetes services from API watcher")
|
||||
}
|
||||
|
||||
// We only care about LoadBalancer services
|
||||
if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
|
||||
break
|
||||
}
|
||||
|
||||
svcAddresses := fetchServiceAddresses(svc)
|
||||
|
||||
// We only care about LoadBalancer services that have been allocated an address
|
||||
if len(svcAddresses) <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Check the loadBalancer class
|
||||
if svc.Spec.LoadBalancerClass != nil {
|
||||
// if this isn't nil then it has been configured, check if it the kube-vip loadBalancer class
|
||||
if *svc.Spec.LoadBalancerClass != sm.config.LoadBalancerClassName {
|
||||
log.Infof("(svcs) [%s] specified the loadBalancer class [%s], ignoring", svc.Name, *svc.Spec.LoadBalancerClass)
|
||||
break
|
||||
}
|
||||
} else if sm.config.LoadBalancerClassOnly {
|
||||
// if kube-vip is configured to only recognize services with kube-vip's lb class, then ignore the services without any lb class
|
||||
log.Infof("(svcs) kube-vip configured to only recognize services with kube-vip's lb class but the service [%s] didn't specify any loadBalancer class, ignoring", svc.Name)
|
||||
break
|
||||
}
|
||||
|
||||
// Check if we ignore this service
|
||||
if svc.Annotations["kube-vip.io/ignore"] == "true" {
|
||||
log.Infof("(svcs) [%s] has an ignore annotation for kube-vip", svc.Name)
|
||||
break
|
||||
}
|
||||
|
||||
// The modified event should only be triggered if the service has been modified (i.e. moved somewhere else)
|
||||
if event.Type == watch.Modified {
|
||||
for _, addr := range svcAddresses {
|
||||
//log.Debugf("(svcs) Retreiving local addresses, to ensure that this modified address doesn't exist: %s", addr)
|
||||
f, err := vip.GarbageCollect(sm.config.Interface, addr)
|
||||
if err != nil {
|
||||
log.Errorf("(svcs) cleaning existing address error: [%s]", err.Error())
|
||||
}
|
||||
if f {
|
||||
log.Warnf("(svcs) already found existing address [%s] on adapter [%s]", addr, sm.config.Interface)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Scenarios:
|
||||
// 1.
|
||||
if !activeService[string(svc.UID)] {
|
||||
log.Debugf("(svcs) [%s] has been added/modified with addresses [%s]", svc.Name, fetchServiceAddresses(svc))
|
||||
|
||||
wg.Add(1)
|
||||
activeServiceLoadBalancer[string(svc.UID)], activeServiceLoadBalancerCancel[string(svc.UID)] = context.WithCancel(context.TODO())
|
||||
// Background the services election
|
||||
// EnableServicesElection enabled
|
||||
// watchEndpoint will do a ServicesElection by Service and understands local endpoints
|
||||
//
|
||||
// EnableRoutingTable enabled and EnableLeaderElection disabled
|
||||
// watchEndpoint will also not do a leaderElection by service.
|
||||
if sm.config.EnableServicesElection ||
|
||||
((sm.config.EnableRoutingTable || sm.config.EnableBGP) && (!sm.config.EnableLeaderElection && !sm.config.EnableServicesElection)) {
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
|
||||
// Start an endpoint watcher if we're not watching it already
|
||||
if !watchedService[string(svc.UID)] {
|
||||
// background the endpoint watcher
|
||||
go func() {
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
|
||||
// Add Endpoint or EndpointSlices watcher
|
||||
wg.Add(1)
|
||||
var provider epProvider
|
||||
if !sm.config.EnableEndpointSlices {
|
||||
provider = &endpointsProvider{label: "endpoints"}
|
||||
|
||||
} else {
|
||||
provider = &endpointslicesProvider{label: "endpointslices"}
|
||||
}
|
||||
if err = sm.watchEndpoint(activeServiceLoadBalancer[string(svc.UID)], id, svc, &wg, provider); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
if (sm.config.EnableRoutingTable || sm.config.EnableBGP) && (!sm.config.EnableLeaderElection && !sm.config.EnableServicesElection) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = serviceFunc(activeServiceLoadBalancer[string(svc.UID)], svc, &wg)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
// We're now watching this service
|
||||
watchedService[string(svc.UID)] = true
|
||||
}
|
||||
} else if (sm.config.EnableBGP || sm.config.EnableRoutingTable) && (!sm.config.EnableLeaderElection && !sm.config.EnableServicesElection) {
|
||||
go func() {
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
// Add Endpoint watcher
|
||||
wg.Add(1)
|
||||
var provider epProvider
|
||||
if !sm.config.EnableEndpointSlices {
|
||||
provider = &endpointsProvider{label: "endpoints"}
|
||||
|
||||
} else {
|
||||
provider = &endpointslicesProvider{label: "endpointslices"}
|
||||
}
|
||||
if err = sm.watchEndpoint(activeServiceLoadBalancer[string(svc.UID)], id, svc, &wg, provider); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = serviceFunc(activeServiceLoadBalancer[string(svc.UID)], svc, &wg)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
} else {
|
||||
// Increment the waitGroup before the service Func is called (Done is completed in there)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err = serviceFunc(activeServiceLoadBalancer[string(svc.UID)], svc, &wg)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
activeService[string(svc.UID)] = true
|
||||
} else {
|
||||
// Increment the waitGroup before the service Func is called (Done is completed in there)
|
||||
wg.Add(1)
|
||||
err = serviceFunc(activeServiceLoadBalancer[string(svc.UID)], svc, &wg)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}
|
||||
case watch.Deleted:
|
||||
svc, ok := event.Object.(*v1.Service)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to parse Kubernetes services from API watcher")
|
||||
}
|
||||
if activeService[string(svc.UID)] {
|
||||
|
||||
// We only care about LoadBalancer services
|
||||
if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
|
||||
break
|
||||
}
|
||||
|
||||
// We can ignore this service
|
||||
if svc.Annotations["kube-vip.io/ignore"] == "true" {
|
||||
log.Infof("(svcs) [%s] has an ignore annotation for kube-vip", svc.Name)
|
||||
break
|
||||
}
|
||||
|
||||
isRouteConfigured, err := isRouteConfigured(svc.UID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while checkig if route is configured: %w", err)
|
||||
}
|
||||
// If no leader election is enabled, delete routes here
|
||||
if !sm.config.EnableLeaderElection && !sm.config.EnableServicesElection &&
|
||||
sm.config.EnableRoutingTable && isRouteConfigured {
|
||||
if errs := sm.clearRoutes(svc); len(errs) == 0 {
|
||||
configuredLocalRoutes.Store(string(svc.UID), false)
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an active service then and additional leaderElection will handle stopping
|
||||
err = sm.deleteService(string(svc.UID))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
// Calls the cancel function of the context
|
||||
if activeServiceLoadBalancerCancel[string(svc.UID)] != nil {
|
||||
activeServiceLoadBalancerCancel[string(svc.UID)]()
|
||||
}
|
||||
activeService[string(svc.UID)] = false
|
||||
watchedService[string(svc.UID)] = false
|
||||
}
|
||||
|
||||
if (sm.config.EnableBGP || sm.config.EnableRoutingTable) && sm.config.EnableLeaderElection && !sm.config.EnableServicesElection {
|
||||
if sm.config.EnableBGP {
|
||||
instance := sm.findServiceInstance(svc)
|
||||
for _, vip := range instance.vipConfigs {
|
||||
vipCidr := fmt.Sprintf("%s/%s", vip.VIP, vip.VIPCIDR)
|
||||
err = sm.bgpServer.DelHost(vipCidr)
|
||||
if err != nil {
|
||||
log.Errorf("error deleting host %s: %s", vipCidr, err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.clearRoutes(svc)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("(svcs) [%s/%s] has been deleted", svc.Namespace, svc.Name)
|
||||
case watch.Bookmark:
|
||||
// Un-used
|
||||
case watch.Error:
|
||||
log.Error("Error attempting to watch Kubernetes services")
|
||||
|
||||
// This round trip allows us to handle unstructured status
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, ok := errObject.(*apierrors.StatusError)
|
||||
if !ok {
|
||||
log.Errorf(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
|
||||
}
|
||||
|
||||
status := statusErr.ErrStatus
|
||||
log.Errorf("services -> %v", status)
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(exitFunction)
|
||||
log.Warnln("Stopping watching services for type: LoadBalancer in all namespaces")
|
||||
return nil
|
||||
}
|
||||
|
||||
func isRouteConfigured(serviceUID types.UID) (bool, error) {
|
||||
isConfigured := false
|
||||
value, ok := configuredLocalRoutes.Load(string(serviceUID))
|
||||
if ok {
|
||||
isConfigured, ok = value.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("error converting configuredLocalRoute item to boolean value")
|
||||
}
|
||||
}
|
||||
|
||||
return isConfigured, nil
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -15,7 +15,7 @@ func TestParseBgpAnnotations(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Annotations: map[string]string{}},
|
||||
}
|
||||
|
||||
bgpConfigBase := bgp.Config{
|
||||
bgpConfigBase := kubevip.BGPConfig{
|
||||
HoldTime: 15,
|
||||
KeepaliveInterval: 5,
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func TestParseBgpAnnotations(t *testing.T) {
|
||||
t.Fatal("Parsing BGP annotations should return nil when minimum config is met")
|
||||
}
|
||||
|
||||
bgpPeers := []bgp.Peer{
|
||||
bgpPeers := []kubevip.BGPPeer{
|
||||
{Address: "10.0.0.1", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.2", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.3", AS: uint32(64000), Password: "password"},
|
||||
@@ -77,7 +77,7 @@ func TestParseNewBgpAnnotations(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Annotations: map[string]string{}},
|
||||
}
|
||||
|
||||
bgpConfigBase := bgp.Config{
|
||||
bgpConfigBase := kubevip.BGPConfig{
|
||||
HoldTime: 15,
|
||||
KeepaliveInterval: 5,
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestParseNewBgpAnnotations(t *testing.T) {
|
||||
t.Fatalf("Parsing BGP annotations should return nil when minimum config is met [%v]", err)
|
||||
}
|
||||
|
||||
bgpPeers := []bgp.Peer{
|
||||
bgpPeers := []kubevip.BGPPeer{
|
||||
{Address: "10.0.0.1", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.2", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.3", AS: uint32(64000), Password: "password"},
|
||||
@@ -121,15 +121,15 @@ func Test_parseBgpAnnotations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bgp.Config
|
||||
want1 bgp.Peer
|
||||
want kubevip.BGPConfig
|
||||
want1 kubevip.BGPPeer
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := parseBgpAnnotations(bgp.Config{}, tt.args.node, tt.args.prefix)
|
||||
got, got1, err := parseBgpAnnotations(kubevip.BGPConfig{}, tt.args.node, tt.args.prefix)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseBgpAnnotations() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
34
pkg/networkinterface/networkinterface.go
Normal file
34
pkg/networkinterface/networkinterface.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package networkinterface
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
interfaces map[string]*Link
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Lock sync.Mutex
|
||||
Intf netlink.Link
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
interfaces: make(map[string]*Link),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Get(intf netlink.Link) *Link {
|
||||
if l, ok := m.interfaces[intf.Attrs().Name]; ok {
|
||||
return l
|
||||
}
|
||||
result := &Link{
|
||||
Intf: intf,
|
||||
}
|
||||
|
||||
m.interfaces[intf.Attrs().Name] = result
|
||||
return result
|
||||
}
|
||||
486
pkg/nftables/nftables.go
Normal file
486
pkg/nftables/nftables.go
Normal file
@@ -0,0 +1,486 @@
|
||||
package nftables
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/binaryutil"
|
||||
"github.com/google/nftables/expr"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
NatTable = "kube_vip_%s"
|
||||
SNatChain = "kube_vip_snat_%s"
|
||||
)
|
||||
|
||||
func ApplySNAT(podIP, vipIP, service, destinationPorts string, ignoreCIDR []string, IPv6 bool) error {
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tableName string
|
||||
if IPv6 {
|
||||
tableName = fmt.Sprintf(NatTable, "v6")
|
||||
} else {
|
||||
tableName = fmt.Sprintf(NatTable, "v4")
|
||||
}
|
||||
// Look up the table
|
||||
if t, err := FilterTable(conn, tableName, IPv6); err != nil {
|
||||
if t == nil {
|
||||
// If it doesn't exist then create it
|
||||
slog.Debug("[egress]", "Creating Table", tableName)
|
||||
conn.AddTable(GetTable(IPv6))
|
||||
}
|
||||
}
|
||||
slog.Debug("[egress]", "Creating Chain for service", service, utils.IPv6Family, IPv6)
|
||||
conn.AddChain(GetSNatChain(IPv6, service))
|
||||
err = conn.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Create our nftables rule
|
||||
rule, err := CreateRule(podIP, vipIP, service, destinationPorts, ignoreCIDR, conn, IPv6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Debug("[egress]", "table", rule.Table.Name, "chain", rule.Chain.Name, "expr", rule.Exprs)
|
||||
conn.AddRule(rule) // Add the rule
|
||||
|
||||
err = conn.Flush() // Commit the rule to nftables
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.CloseLasting() // Close out any remaining netlink communication
|
||||
}
|
||||
|
||||
func DeleteSNAT(IPv6 bool, service string) error {
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var chainName = fmt.Sprintf(SNatChain, service)
|
||||
slog.Info("[egress]", "Looking for", chainName)
|
||||
|
||||
chain, err := conn.ListChain(GetTable(IPv6), chainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chain != nil {
|
||||
slog.Info("[egress]", "Deleting chain", chainName)
|
||||
conn.DelChain(chain)
|
||||
return conn.Flush()
|
||||
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to find chain [%s]", chainName)
|
||||
}
|
||||
|
||||
func GetTable(IPv6 bool) *nftables.Table {
|
||||
var tableName string
|
||||
if IPv6 {
|
||||
tableName = fmt.Sprintf(NatTable, "v6")
|
||||
} else {
|
||||
tableName = fmt.Sprintf(NatTable, "v4")
|
||||
}
|
||||
// Default to IPv4
|
||||
table := &nftables.Table{
|
||||
Family: nftables.TableFamilyIPv4,
|
||||
Name: tableName,
|
||||
}
|
||||
|
||||
// Move to IPv6 if needed
|
||||
if IPv6 {
|
||||
table.Family = nftables.TableFamilyIPv6
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
func GetSNatChain(IPv6 bool, service string) *nftables.Chain {
|
||||
var chainName = fmt.Sprintf(SNatChain, service)
|
||||
policy := nftables.ChainPolicyAccept
|
||||
return &nftables.Chain{
|
||||
Name: chainName,
|
||||
Table: GetTable(IPv6),
|
||||
Type: nftables.ChainTypeNAT,
|
||||
Hooknum: nftables.ChainHookPostrouting,
|
||||
Priority: nftables.ChainPriorityNATSource,
|
||||
Policy: &policy,
|
||||
}
|
||||
}
|
||||
|
||||
func FilterTable(conn *nftables.Conn, tableName string, IPv6 bool) (*nftables.Table, error) {
|
||||
if IPv6 {
|
||||
return conn.ListTableOfFamily(tableName, nftables.TableFamilyIPv6)
|
||||
}
|
||||
return conn.ListTableOfFamily(tableName, nftables.TableFamilyIPv4)
|
||||
}
|
||||
|
||||
// ClearTable will remove the original tables and create new empty ones
|
||||
func ClearTable(conn *nftables.Conn) error {
|
||||
tableName := fmt.Sprintf(NatTable, "v6")
|
||||
if t, err := FilterTable(conn, tableName, false); err != nil {
|
||||
return err
|
||||
} else if t != nil {
|
||||
conn.DelTable(t)
|
||||
}
|
||||
|
||||
// These don't return errors, so not 100% sure how to guarantee things were created
|
||||
conn.AddTable(GetTable(true))
|
||||
tableName = fmt.Sprintf(NatTable, "v4")
|
||||
if t, err := FilterTable(conn, tableName, true); err != nil {
|
||||
return err
|
||||
} else if t != nil {
|
||||
conn.DelTable(t)
|
||||
}
|
||||
|
||||
// These don't return errors, so not 100% sure how to guarantee things were created
|
||||
conn.AddTable(GetTable(false))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create our nftables rule
|
||||
func CreateRule(podIP, vipIP, service, destinationPorts string, ignoreCIDR []string, conn *nftables.Conn, IPv6 bool) (*nftables.Rule, error) {
|
||||
|
||||
// Validate pod IP
|
||||
if net.ParseIP(podIP) == nil {
|
||||
return nil, errors.New("ip is invalid")
|
||||
}
|
||||
|
||||
// Validate vip IP
|
||||
if net.ParseIP(vipIP) == nil {
|
||||
return nil, errors.New("output_ip is not a valid ip")
|
||||
}
|
||||
|
||||
// Get the kube-vip table
|
||||
table := GetTable(IPv6)
|
||||
|
||||
// Create our rule
|
||||
rule := &nftables.Rule{
|
||||
Table: table,
|
||||
Exprs: []expr.Any{},
|
||||
}
|
||||
// Set the correct chain
|
||||
rule.Chain = GetSNatChain(IPv6, service)
|
||||
|
||||
// Create a set for our original/source address
|
||||
set := &nftables.Set{
|
||||
Table: table,
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
KeyType: nftables.TypeIPAddr,
|
||||
Interval: false,
|
||||
}
|
||||
if IPv6 {
|
||||
set.KeyType = nftables.TypeIP6Addr
|
||||
} else {
|
||||
set.KeyType = nftables.TypeIPAddr
|
||||
}
|
||||
|
||||
// Create an element using our pod IP
|
||||
elements := []nftables.SetElement{}
|
||||
if IPv6 {
|
||||
elements = append(elements, nftables.SetElement{Key: net.ParseIP(podIP).To16()})
|
||||
} else {
|
||||
elements = append(elements, nftables.SetElement{Key: net.ParseIP(podIP).To4()})
|
||||
}
|
||||
|
||||
// Add the elements to the set
|
||||
err := conn.AddSet(set, elements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the expression using the set
|
||||
expression := []expr.Any{}
|
||||
|
||||
payload := &expr.Payload{
|
||||
OperationType: expr.PayloadLoad,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
DestRegister: 1,
|
||||
SourceRegister: 0,
|
||||
}
|
||||
|
||||
// Set the length of the data based upon the type of IP version being used
|
||||
if IPv6 {
|
||||
payload.Offset = 8
|
||||
payload.Len = 16
|
||||
} else {
|
||||
payload.Offset = 12
|
||||
payload.Len = 4
|
||||
}
|
||||
lookup := &expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
DestRegister: 0,
|
||||
SetID: set.ID,
|
||||
}
|
||||
|
||||
// Add expressions
|
||||
expression = append(expression, payload)
|
||||
expression = append(expression, lookup)
|
||||
|
||||
// Add expression to the rule
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
|
||||
// If we filter on ports protocols then parse them
|
||||
if destinationPorts != "" {
|
||||
fixedPorts := strings.Split(destinationPorts, ",")
|
||||
|
||||
// Create an element using our pod IP
|
||||
tcpElements := []nftables.SetElement{}
|
||||
udpElements := []nftables.SetElement{}
|
||||
sctpElements := []nftables.SetElement{}
|
||||
|
||||
tcpSet := &nftables.Set{
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
Table: table,
|
||||
KeyType: nftables.TypeInetService,
|
||||
}
|
||||
udpSet := &nftables.Set{
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
Table: table,
|
||||
KeyType: nftables.TypeInetService,
|
||||
}
|
||||
sctpSet := &nftables.Set{
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
Table: table,
|
||||
KeyType: nftables.TypeInetService,
|
||||
}
|
||||
for _, fixedPort := range fixedPorts {
|
||||
data := strings.Split(fixedPort, ":")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
} else if len(data) == 2 { // Ensure we have two elements { proto:port }
|
||||
// parse the port to a number
|
||||
port, err := strconv.Atoi(data[1])
|
||||
if err != nil {
|
||||
slog.Error("[egress]", "unable to process port", data[1])
|
||||
continue
|
||||
}
|
||||
// Ensure the port is within the valid range for uint16
|
||||
if port < 0 || port > 65535 {
|
||||
slog.Error("[egress]", "port out of range for uint16", data[1])
|
||||
continue
|
||||
}
|
||||
|
||||
switch data[0] {
|
||||
case "tcp":
|
||||
//nolint:gosec
|
||||
tcpElements = append(tcpElements, nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(uint16(port))})
|
||||
case "udp":
|
||||
//nolint:gosec
|
||||
udpElements = append(udpElements, nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(uint16(port))})
|
||||
case "sctp":
|
||||
//nolint:gosec
|
||||
sctpElements = append(sctpElements, nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(uint16(port))})
|
||||
default:
|
||||
slog.Error("[egress]", "unknown protocol", data[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add TCP Ports
|
||||
if len(tcpElements) != 0 {
|
||||
err = conn.AddSet(tcpSet, tcpElements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
// [ cmp eq reg 1 0x00000006 ]
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{unix.IPPROTO_TCP},
|
||||
},
|
||||
|
||||
// [ payload load 2b @ transport header + 2 => reg 1 ]
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
// [ lookup reg 1 set __set%d ]
|
||||
&expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
SetName: tcpSet.Name,
|
||||
SetID: tcpSet.ID,
|
||||
},
|
||||
}
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
|
||||
// Add UDP ports
|
||||
if len(udpElements) != 0 {
|
||||
err = conn.AddSet(udpSet, udpElements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
// [ cmp eq reg 1 0x00000006 ]
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{unix.IPPROTO_UDP},
|
||||
},
|
||||
|
||||
// [ payload load 2b @ transport header + 2 => reg 1 ]
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
// [ lookup reg 1 set __set%d ]
|
||||
&expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
SetName: udpSet.Name,
|
||||
SetID: udpSet.ID,
|
||||
},
|
||||
}
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
|
||||
// Add SCTP Ports
|
||||
if len(sctpElements) != 0 {
|
||||
err = conn.AddSet(sctpSet, sctpElements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
// [ cmp eq reg 1 0x00000006 ]
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{unix.IPPROTO_SCTP},
|
||||
},
|
||||
|
||||
// [ payload load 2b @ transport header + 2 => reg 1 ]
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
// [ lookup reg 1 set __set%d ]
|
||||
&expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
SetName: sctpSet.Name,
|
||||
SetID: sctpSet.ID,
|
||||
},
|
||||
}
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse which CIDRs we will not SNAT for
|
||||
for _, cidr := range ignoreCIDR {
|
||||
start, end, err := nftables.NetFirstAndLastIP(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression = []expr.Any{}
|
||||
|
||||
payload := &expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
}
|
||||
notEqualRange := &expr.Range{
|
||||
Op: expr.CmpOpNeq,
|
||||
Register: 1,
|
||||
}
|
||||
|
||||
if IPv6 {
|
||||
payload.Len = 16
|
||||
payload.Offset = 24
|
||||
notEqualRange.FromData = start.To16()
|
||||
notEqualRange.ToData = end.To16()
|
||||
} else {
|
||||
payload.Offset = 16
|
||||
payload.Len = 4
|
||||
notEqualRange.FromData = start.To4()
|
||||
notEqualRange.ToData = end.To4()
|
||||
}
|
||||
// Add expressions
|
||||
expression = append(expression, payload)
|
||||
expression = append(expression, notEqualRange)
|
||||
|
||||
// // Add expression to the rule
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
|
||||
// Final expression to the rule is the SNAT to the VIP address
|
||||
expression = []expr.Any{}
|
||||
|
||||
immediate := &expr.Immediate{
|
||||
Register: 1,
|
||||
}
|
||||
|
||||
nat := &expr.NAT{
|
||||
Type: expr.NATTypeSourceNAT,
|
||||
RegAddrMin: 1,
|
||||
RegAddrMax: 1,
|
||||
RegProtoMin: 0,
|
||||
RegProtoMax: 0,
|
||||
Random: false,
|
||||
FullyRandom: false,
|
||||
Persistent: false,
|
||||
Prefix: false,
|
||||
}
|
||||
|
||||
if IPv6 {
|
||||
immediate.Data = net.ParseIP(vipIP).To16()
|
||||
nat.Family = unix.NFPROTO_IPV6
|
||||
} else {
|
||||
immediate.Data = net.ParseIP(vipIP).To4()
|
||||
nat.Family = unix.NFPROTO_IPV4
|
||||
}
|
||||
// https://github.com/google/nftables/blob/main/nftables_test.go#L5375
|
||||
// Add expressions
|
||||
expression = append(expression, immediate)
|
||||
expression = append(expression, nat)
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
// Returns a list of all chains IPv4/IPv6 in nftables
|
||||
func ListChains() ([]string, error) {
|
||||
chains := []string{}
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipv4, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipv6, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for x := range ipv4 {
|
||||
chains = append(chains, fmt.Sprintf("Table=%s, Chain=%s", ipv4[x].Table.Name, ipv4[x].Name))
|
||||
}
|
||||
for x := range ipv6 {
|
||||
chains = append(chains, fmt.Sprintf("Table=%s, Chain=%s", ipv6[x].Table.Name, ipv6[x].Name))
|
||||
}
|
||||
|
||||
_ = conn.CloseLasting() // TODO: Should we ignore this error, we're not actually doing any actions with nftables
|
||||
return chains, nil
|
||||
}
|
||||
164
pkg/node/labeler/label_manager.go
Normal file
164
pkg/node/labeler/label_manager.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package labeler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
// labelName is the name of the label that will be added to the node
|
||||
// it is prefix for the label key before "/"
|
||||
labelName = "service-provided.kube-vip.io"
|
||||
)
|
||||
|
||||
// labelOperation is the operation to perform on the node labels
|
||||
type labelOperation string
|
||||
|
||||
// labelOperation constants
|
||||
const (
|
||||
labelOperationRemove labelOperation = "remove"
|
||||
labelOperationAdd labelOperation = "add"
|
||||
)
|
||||
|
||||
// NewManager creates a new Label Manager for the given node
|
||||
func NewManager(nodeName string, clientSet *kubernetes.Clientset) *Manager {
|
||||
return &Manager{
|
||||
nodeName: nodeName,
|
||||
clientSet: clientSet,
|
||||
}
|
||||
}
|
||||
|
||||
// Manager is the label Manager for the node
|
||||
type Manager struct {
|
||||
// nodeName is the name of the node to manage
|
||||
nodeName string
|
||||
|
||||
// clientSet is the Kubernetes client set to use
|
||||
clientSet *kubernetes.Clientset
|
||||
}
|
||||
|
||||
// AddLabel a new label to the node
|
||||
func (m *Manager) AddLabel(ctx context.Context, svc *corev1.Service) error {
|
||||
log.Debug("[service] add label to node", "namespace", svc.Namespace, "name", svc.Name)
|
||||
labelKey, labelValue := generateNodeLabelKeyValue(svc)
|
||||
return m.patchNode(ctx, labelOperationAdd, map[string]string{labelKey: labelValue})
|
||||
}
|
||||
|
||||
// RemoveLabel a label from the node
|
||||
func (m *Manager) RemoveLabel(ctx context.Context, svc *corev1.Service) error {
|
||||
log.Debug("[service] delete label from node", "namespace", svc.Namespace, "name", svc.Name)
|
||||
labelKey, _ := generateNodeLabelKeyValue(svc)
|
||||
return m.patchNode(ctx, labelOperationRemove, map[string]string{labelKey: ""})
|
||||
}
|
||||
|
||||
// clean up the node labels
|
||||
func (m *Manager) CleanUpLabels(timeout time.Duration) error {
|
||||
log.Debug("cleaning up labels for node", "node", m.nodeName, "timeout", timeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// get the node
|
||||
node, err := m.clientSet.CoreV1().Nodes().Get(ctx, m.nodeName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get node %s", m.nodeName)
|
||||
}
|
||||
|
||||
// collect all labels with the prefix to remove
|
||||
labels := map[string]string{}
|
||||
for k := range node.Labels {
|
||||
if strings.HasPrefix(k, labelName) {
|
||||
labels[k] = ""
|
||||
}
|
||||
}
|
||||
|
||||
if len(labels) == 0 {
|
||||
log.Debug("no labels to remove for node", "node", m.nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// patch the node with the labels to remove
|
||||
return m.patchNode(ctx, labelOperationRemove, labels)
|
||||
}
|
||||
|
||||
// generateNodeLabelKeyValue generates a label key and value for the given service
|
||||
func generateNodeLabelKeyValue(svc *corev1.Service) (string, string) {
|
||||
addresses, _ := instance.FetchServiceAddresses(svc)
|
||||
|
||||
sanitized := make([]string, len(addresses))
|
||||
for i, addr := range addresses {
|
||||
sanitized[i] = sanitizeIPForLabel(addr)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s.%s", labelName, svc.Name, svc.Namespace), strings.Join(sanitized, ",")
|
||||
}
|
||||
|
||||
// Helper function to convert IPv6 hex address without colons
|
||||
func sanitizeIPForLabel(addr string) string {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil || ip.To4() != nil {
|
||||
return addr
|
||||
}
|
||||
return hex.EncodeToString(ip.To16())
|
||||
}
|
||||
|
||||
// patchNode patches the node with the given labels
|
||||
func (m *Manager) patchNode(ctx context.Context, operation labelOperation, labels map[string]string) error {
|
||||
type patchStringLabel struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
patchLabels := []patchStringLabel{}
|
||||
// generate the patch
|
||||
for k, v := range labels {
|
||||
patchLabels = append(patchLabels, patchStringLabel{
|
||||
Op: string(operation),
|
||||
// replace all slashes with ~1
|
||||
Path: fmt.Sprintf("/metadata/labels/%s", strings.ReplaceAll(k, "/", "~1")),
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
patchData, err := json.Marshal(patchLabels)
|
||||
if err != nil {
|
||||
log.Debug("node patch marshaling failed", "err", err, "labels", labels, "patch", patchLabels)
|
||||
return errors.Wrapf(err, "node patch marshaling failed for labels %v", labels)
|
||||
}
|
||||
|
||||
log.Debug("patching node",
|
||||
"node", m.nodeName,
|
||||
"patch", string(patchData),
|
||||
"operation", operation,
|
||||
"labels", labels,
|
||||
"clientSetNil", m.clientSet == nil)
|
||||
if m.clientSet == nil {
|
||||
return errors.New("kubernetes client is not initialized")
|
||||
}
|
||||
|
||||
// patch node
|
||||
node, err := m.clientSet.CoreV1().Nodes().Patch(ctx, m.nodeName, types.JSONPatchType, patchData, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
log.Debug("node patching failed", "err", err, "patchData", patchData)
|
||||
return errors.Wrapf(err, "node patching failed with patch %s", string(patchData))
|
||||
}
|
||||
|
||||
log.Debug("updated", "node", m.nodeName, "labels", node.Labels)
|
||||
|
||||
return nil
|
||||
}
|
||||
27
pkg/node/noop/label_noop_manager.go
Normal file
27
pkg/node/noop/label_noop_manager.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package noop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// NewManager creates a new NoOp label manager
|
||||
func NewManager() *Manager {
|
||||
return &Manager{}
|
||||
}
|
||||
|
||||
type Manager struct{}
|
||||
|
||||
func (m *Manager) AddLabel(_ context.Context, _ *corev1.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveLabel(_ context.Context, _ *corev1.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CleanUpLabels(_ time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
49
pkg/node/types.go
Normal file
49
pkg/node/types.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/node/labeler"
|
||||
"github.com/kube-vip/kube-vip/pkg/node/noop"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// LabelManager is the interface for the node label manager
|
||||
type LabelManager interface {
|
||||
// AddLabel adds a label to the node for the given service
|
||||
AddLabel(ctx context.Context, svc *v1.Service) error
|
||||
|
||||
// RemoveLabel removes the label from the node for the given service
|
||||
RemoveLabel(ctx context.Context, svc *v1.Service) error
|
||||
|
||||
// CleanUpLabels removes all labels from the node
|
||||
CleanUpLabels(timeout time.Duration) error
|
||||
}
|
||||
|
||||
// NewManager creates a new Label Manager for the given node
|
||||
// NoOp implementation is returned if node labeling is disabled, or if
|
||||
// running in control plane mode, or if the client is not ready
|
||||
func NewManager(config *kubevip.Config, clientSet *kubernetes.Clientset) LabelManager {
|
||||
if !config.EnableNodeLabeling {
|
||||
return noop.NewManager()
|
||||
}
|
||||
|
||||
if config.EnableControlPlane {
|
||||
log.Debug("Skip node labeling, control plane mode enabled")
|
||||
return noop.NewManager()
|
||||
}
|
||||
|
||||
if clientSet == nil {
|
||||
log.Debug("Skip node labeling, client is not ready")
|
||||
return noop.NewManager()
|
||||
}
|
||||
|
||||
log.Debug("Node labeling enabled")
|
||||
|
||||
return labeler.NewManager(config.NodeName, clientSet)
|
||||
}
|
||||
36
pkg/servicecontext/servicecontext.go
Normal file
36
pkg/servicecontext/servicecontext.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package servicecontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
IsActive bool
|
||||
IsWatched bool
|
||||
ConfiguredNetworks sync.Map
|
||||
}
|
||||
|
||||
func New(ctx context.Context) *Context {
|
||||
svcCtx, svcCancel := context.WithCancel(ctx)
|
||||
return &Context{
|
||||
Ctx: svcCtx,
|
||||
Cancel: svcCancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *Context) HasConfiguredNetworks() bool {
|
||||
cnt := 0
|
||||
ctx.ConfiguredNetworks.Range(func(_ any, _ any) bool {
|
||||
cnt++
|
||||
return cnt < 1
|
||||
})
|
||||
return cnt > 0
|
||||
}
|
||||
|
||||
func (ctx *Context) IsNetworkConfigured(ip string) bool {
|
||||
_, exists := ctx.ConfiguredNetworks.Load(ip)
|
||||
return exists
|
||||
}
|
||||
361
pkg/services/egress.go
Normal file
361
pkg/services/egress.go
Normal file
@@ -0,0 +1,361 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/iptables"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/nftables"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// DEBUG
|
||||
const (
|
||||
defaultPodCIDR = "10.0.0.0/16"
|
||||
defaultServiceCIDR = "10.96.0.0/12"
|
||||
)
|
||||
|
||||
func (p *Processor) iptablesCheck() error {
|
||||
file, err := os.Open("/proc/modules")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
var nat, filter, mangle bool
|
||||
for scanner.Scan() {
|
||||
line := strings.Fields(scanner.Text())
|
||||
switch line[0] {
|
||||
case "iptable_filter":
|
||||
filter = true
|
||||
case "iptable_nat":
|
||||
nat = true
|
||||
case "iptable_mangle":
|
||||
mangle = true
|
||||
}
|
||||
}
|
||||
|
||||
if !filter || !nat || !mangle {
|
||||
return fmt.Errorf("missing iptables modules -> nat [%t] -> filter [%t] mangle -> [%t]", nat, filter, mangle)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) nftablesCheck() error {
|
||||
file, err := os.Open("/proc/modules")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
var queue, ct bool
|
||||
for scanner.Scan() {
|
||||
line := strings.Fields(scanner.Text())
|
||||
switch line[0] {
|
||||
case "nft_queue":
|
||||
queue = true
|
||||
case "nft_ct":
|
||||
ct = true
|
||||
}
|
||||
}
|
||||
|
||||
if !queue || !ct {
|
||||
return fmt.Errorf("missing nftables modules -> nft_ct [%t] -> ntf_queue [%t]", ct, queue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSameFamilyCidr(sourceCidrs, ip string) string { //Todo: not sure how this ever worked
|
||||
cidrs := strings.Split(sourceCidrs, ",")
|
||||
isV6 := utils.IsIPv6(ip)
|
||||
matchingFamily := []string{}
|
||||
for _, cidr := range cidrs {
|
||||
// Is the ip an IPv6 address
|
||||
if isV6 {
|
||||
if utils.IsIPv6CIDR(cidr) {
|
||||
matchingFamily = append(matchingFamily, cidr)
|
||||
selectedCIDR, err := checkCIDR(ip, cidr)
|
||||
if err != nil {
|
||||
log.Warn("IPv6 CIDR check ", "err", err)
|
||||
continue
|
||||
}
|
||||
if selectedCIDR != "" {
|
||||
return selectedCIDR
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if utils.IsIPv4CIDR(cidr) {
|
||||
matchingFamily = append(matchingFamily, cidr)
|
||||
selectedCidr, err := checkCIDR(ip, cidr)
|
||||
if err != nil {
|
||||
log.Warn("IPv4 CIDR check ", "err", err)
|
||||
continue
|
||||
}
|
||||
if selectedCidr != "" {
|
||||
return selectedCidr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(matchingFamily) > 0 {
|
||||
// return first CIDR that has at least the same IP family as processed IP address
|
||||
// (should be better than just returning first CIDR on the list, I think)
|
||||
return matchingFamily[0]
|
||||
}
|
||||
|
||||
// return to the default behaviour of setting the CIDR to the first one (or only one)
|
||||
return cidrs[0]
|
||||
}
|
||||
|
||||
func checkCIDR(ip, cidr string) (string, error) {
|
||||
_, ipnetA, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse CIDR [%s]: %w", cidr, err)
|
||||
}
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return "", fmt.Errorf("failed to parse IP [%s]", ip)
|
||||
}
|
||||
if ipnetA.Contains(parsedIP) {
|
||||
return cidr, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *Processor) configureEgress(vipIP, podIP, namespace, serviceUUID string, annotations map[string]string) error {
|
||||
var podCidr, serviceCidr string
|
||||
var autoServiceCIDR, autoPodCIDR string
|
||||
var discoverErr error
|
||||
|
||||
// Look up the destination ports from the annotations on the service
|
||||
destinationPorts := annotations[kubevip.EgressDestinationPorts]
|
||||
deniedNetworks := annotations[kubevip.EgressDeniedNetworks]
|
||||
allowedNetworks := annotations[kubevip.EgressAllowedNetworks]
|
||||
internalEgress := annotations[kubevip.EgressInternal]
|
||||
|
||||
if p.config.EgressPodCidr == "" || p.config.EgressServiceCidr == "" {
|
||||
autoServiceCIDR, autoPodCIDR, discoverErr = p.AutoDiscoverCIDRs()
|
||||
}
|
||||
|
||||
if discoverErr != nil {
|
||||
log.Warn("autodiscover CIDR", "err", discoverErr)
|
||||
}
|
||||
|
||||
if p.config.EgressPodCidr != "" {
|
||||
podCidr = getSameFamilyCidr(p.config.EgressPodCidr, podIP)
|
||||
} else {
|
||||
if discoverErr == nil {
|
||||
podCidr = getSameFamilyCidr(autoPodCIDR, podIP)
|
||||
}
|
||||
}
|
||||
|
||||
if podCidr == "" {
|
||||
// There's no default IPv6 pod CIDR, therefore we silently back off if CIDR s not specified.
|
||||
if !utils.IsIPv4(podIP) {
|
||||
return fmt.Errorf("error with the CIDR [%s]", podIP)
|
||||
}
|
||||
podCidr = defaultPodCIDR
|
||||
}
|
||||
|
||||
if p.config.EgressServiceCidr != "" {
|
||||
serviceCidr = getSameFamilyCidr(p.config.EgressServiceCidr, vipIP)
|
||||
} else {
|
||||
if discoverErr == nil {
|
||||
serviceCidr = getSameFamilyCidr(autoServiceCIDR, vipIP)
|
||||
}
|
||||
}
|
||||
|
||||
if serviceCidr == "" {
|
||||
// There's no default IPv6 service CIDR, therefore we silently back off if CIDR s not specified.
|
||||
if !utils.IsIPv4(vipIP) {
|
||||
return nil
|
||||
}
|
||||
serviceCidr = defaultServiceCIDR
|
||||
}
|
||||
|
||||
log.Info("[Egress]", "podCIDR", podCidr, "serviceCIDR", serviceCidr, "vip", serviceCidr, "pod", podIP)
|
||||
|
||||
// checking if all addresses are of the same IP family
|
||||
if utils.IsIPv4(podIP) != utils.IsIPv4CIDR(podCidr) {
|
||||
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "podCIDR", podCidr)
|
||||
return nil
|
||||
}
|
||||
|
||||
if utils.IsIPv4(vipIP) != utils.IsIPv4CIDR(serviceCidr) {
|
||||
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "serviceCIDR", serviceCidr)
|
||||
return nil
|
||||
}
|
||||
|
||||
if utils.IsIPv4(vipIP) != utils.IsIPv4(podIP) {
|
||||
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "vipIP", vipIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
protocol := iptables.ProtocolIPv4
|
||||
if utils.IsIPv6(vipIP) {
|
||||
protocol = iptables.ProtocolIPv6
|
||||
}
|
||||
|
||||
// Use the internal egress implementation
|
||||
if internalEgress != "" {
|
||||
// Create an array of CIDRs that we wont SNAT to.
|
||||
ignoreCIDRs := []string{
|
||||
podCidr,
|
||||
serviceCidr,
|
||||
}
|
||||
|
||||
// Add any specifically denied networks
|
||||
if deniedNetworks != "" {
|
||||
networks := strings.Split(strings.TrimSpace(deniedNetworks), ",") //Remove whitespace characters and then create an array from the CIDRs
|
||||
ignoreCIDRs = append(ignoreCIDRs, networks...)
|
||||
|
||||
}
|
||||
|
||||
// Apply the SNAT rules
|
||||
err := nftables.ApplySNAT(podIP, vipIP, serviceUUID, destinationPorts, ignoreCIDRs, utils.IsIPv6(vipIP))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error performing netlink nftables [%s]", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
i, err := vip.CreateIptablesClient(p.config.EgressWithNftables, namespace, protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error Creating iptables client [%s]", err)
|
||||
}
|
||||
|
||||
// Check if the kube-vip mangle chain exists, if not create it
|
||||
exists, err := i.CheckMangleChain(vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking for existence of mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
err = i.CreateMangleChain(vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
err = i.AppendReturnRulesForDestinationSubnet(vip.MangleChainName, podCidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
err = i.AppendReturnRulesForDestinationSubnet(vip.MangleChainName, serviceCidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
if deniedNetworks != "" {
|
||||
networks := strings.Split(deniedNetworks, ",")
|
||||
for x := range networks {
|
||||
err = i.AppendReturnRulesForDestinationSubnet(vip.MangleChainName, networks[x])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mask := "/32"
|
||||
if !utils.IsIPv4(podIP) {
|
||||
mask = "/128"
|
||||
}
|
||||
|
||||
if allowedNetworks != "" {
|
||||
networks := strings.Split(allowedNetworks, ",")
|
||||
for x := range networks {
|
||||
err = i.AppendReturnRulesForMarkingForNetwork(vip.MangleChainName, podIP+mask, networks[x])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding marking rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = i.AppendReturnRulesForMarking(vip.MangleChainName, podIP+mask)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding marking rules to mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
err = i.InsertMangeTableIntoPrerouting(vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding prerouting mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
|
||||
if destinationPorts != "" {
|
||||
fixedPorts := strings.Split(destinationPorts, ",")
|
||||
|
||||
for _, fixedPort := range fixedPorts {
|
||||
var proto, port string
|
||||
|
||||
data := strings.Split(fixedPort, ":")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
} else if len(data) == 1 {
|
||||
proto = "tcp"
|
||||
port = data[0]
|
||||
} else {
|
||||
proto = data[0]
|
||||
port = data[1]
|
||||
}
|
||||
|
||||
err = i.InsertSourceNatForDestinationPort(vipIP, podIP, port, proto)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding snat rules to nat chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = i.InsertSourceNat(vipIP, podIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding snat rules to nat chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
//_ = i.DumpChain(vip.MangleChainName)
|
||||
|
||||
err = vip.DeleteExistingSessions(podIP, false, destinationPorts, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) AutoDiscoverCIDRs() (serviceCIDR, podCIDR string, err error) {
|
||||
log.Debug("Trying to automatically discover Service and Pod CIDRs")
|
||||
options := v1.ListOptions{
|
||||
LabelSelector: "component=kube-controller-manager",
|
||||
}
|
||||
podList, err := p.clientSet.CoreV1().Pods("kube-system").List(context.TODO(), options)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("[Egress] Unable to get kube-controller-manager pod: %w", err)
|
||||
}
|
||||
if len(podList.Items) < 1 {
|
||||
return "", "", fmt.Errorf("[Egress] Unable to auto-discover the pod/service CIDRs: kube-controller-manager not found")
|
||||
}
|
||||
|
||||
pod := podList.Items[0]
|
||||
for flags := range pod.Spec.Containers[0].Command {
|
||||
if strings.Contains(pod.Spec.Containers[0].Command[flags], "--cluster-cidr=") {
|
||||
podCIDR = strings.ReplaceAll(pod.Spec.Containers[0].Command[flags], "--cluster-cidr=", "")
|
||||
}
|
||||
if strings.Contains(pod.Spec.Containers[0].Command[flags], "--service-cluster-ip-range=") {
|
||||
serviceCIDR = strings.ReplaceAll(pod.Spec.Containers[0].Command[flags], "--service-cluster-ip-range=", "")
|
||||
}
|
||||
}
|
||||
if podCIDR == "" || serviceCIDR == "" {
|
||||
err = fmt.Errorf("unable to fully determine cluster CIDR configurations")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
151
pkg/services/leader.go
Normal file
151
pkg/services/leader.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/lease"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
)
|
||||
|
||||
// The StartServicesWatchForLeaderElection function will start a services watcher, the
|
||||
func (p *Processor) StartServicesWatchForLeaderElection(ctx context.Context) error {
|
||||
err := p.ServicesWatcher(ctx, p.StartServicesLeaderElection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.config.EnableRoutingTable {
|
||||
for _, instance := range p.ServiceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
_ = cluster.Network[i].DeleteRoute()
|
||||
}
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Shutting down kube-Vip")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// The startServicesWatchForLeaderElection function will start a services watcher, the
|
||||
func (p *Processor) StartServicesLeaderElection(ctx context.Context, service *v1.Service) error {
|
||||
serviceLease, _ := lease.GetName(service)
|
||||
log.Info("new leader election", "service", service.Name, "namespace", service.Namespace, "lock_name", serviceLease, "host_id", p.config.NodeName)
|
||||
// we use the Lease lock type since edits to Leases are less common
|
||||
// and fewer objects in the cluster watch "all Leases".
|
||||
lock := &resourcelock.LeaseLock{
|
||||
LeaseMeta: metav1.ObjectMeta{
|
||||
Name: serviceLease,
|
||||
Namespace: service.Namespace,
|
||||
},
|
||||
Client: p.clientSet.CoordinationV1(),
|
||||
LockConfig: resourcelock.ResourceLockConfig{
|
||||
Identity: p.config.NodeName,
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
// wait for the service context to end and delete the lease then
|
||||
<-ctx.Done()
|
||||
p.leaseMgr.Delete(service)
|
||||
}()
|
||||
|
||||
svcCtx, err := p.getServiceContext(service.UID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get context for service %q with UID %q: %w", service.Name, service.UID, err)
|
||||
}
|
||||
if svcCtx == nil {
|
||||
return fmt.Errorf("failed to get context for service %q with UID %q: nil context", service.Name, service.UID)
|
||||
}
|
||||
|
||||
svcCtx.IsActive = true
|
||||
|
||||
svcLease, isNew := p.leaseMgr.Add(service)
|
||||
// this service is sharing lease
|
||||
if !isNew {
|
||||
// wait for leader election to start or context to be done
|
||||
select {
|
||||
case <-svcLease.Started:
|
||||
case <-svcLease.Ctx.Done():
|
||||
svcCtx.IsActive = false
|
||||
return nil
|
||||
}
|
||||
<-svcLease.Started
|
||||
|
||||
if lease.UsesCommon(service) {
|
||||
if err := p.SyncServices(ctx, service); err != nil {
|
||||
log.Error("service sync", "err", err)
|
||||
svcLease.Cancel()
|
||||
}
|
||||
// just block until context is cancelled
|
||||
<-ctx.Done()
|
||||
if svcCtx.IsActive {
|
||||
if err := p.deleteService(service.UID); err != nil {
|
||||
log.Error("service deletion", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wait for leaderelection to be finished
|
||||
<-svcLease.Ctx.Done()
|
||||
// Mark this service is inactive
|
||||
svcCtx.IsActive = false
|
||||
return nil
|
||||
}
|
||||
// start the leader election code loop
|
||||
leaderelection.RunOrDie(svcLease.Ctx, leaderelection.LeaderElectionConfig{
|
||||
Lock: lock,
|
||||
// IMPORTANT: you MUST ensure that any code you have that
|
||||
// is protected by the lease must terminate **before**
|
||||
// you call cancel. Otherwise, you could have a background
|
||||
// loop still running and another process could
|
||||
// get elected before your background loop finished, violating
|
||||
// the stated goal of the lease.
|
||||
ReleaseOnCancel: true,
|
||||
LeaseDuration: time.Duration(p.config.LeaseDuration) * time.Second,
|
||||
RenewDeadline: time.Duration(p.config.RenewDeadline) * time.Second,
|
||||
RetryPeriod: time.Duration(p.config.RetryPeriod) * time.Second,
|
||||
Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
// Mark this service as active (as we've started leading)
|
||||
// we run this in background as it's blocking
|
||||
if err := p.SyncServices(ctx, service); err != nil {
|
||||
log.Error("service sync", "err", err)
|
||||
svcLease.Cancel()
|
||||
}
|
||||
close(svcLease.Started)
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
// we can do cleanup here
|
||||
log.Info("leadership lost", "service", service.Name, "leader", p.config.NodeName)
|
||||
if svcCtx.IsActive {
|
||||
if err := p.deleteService(service.UID); err != nil {
|
||||
log.Error("service deletion", "err", err)
|
||||
}
|
||||
}
|
||||
// Mark this service is inactive
|
||||
svcCtx.IsActive = false
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
if identity == p.config.NodeName {
|
||||
// I just got the lock
|
||||
return
|
||||
}
|
||||
log.Info("new leader", "leader", identity)
|
||||
},
|
||||
},
|
||||
})
|
||||
log.Info("stopping leader election", "service", service.Name)
|
||||
return nil
|
||||
}
|
||||
357
pkg/services/processor.go
Normal file
357
pkg/services/processor.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/lease"
|
||||
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
config *kubevip.Config
|
||||
lbClassFilter func(svc *v1.Service, config *kubevip.Config) bool
|
||||
svcMap sync.Map
|
||||
|
||||
// Keeps track of all running instances
|
||||
ServiceInstances []*instance.Instance
|
||||
|
||||
mutex sync.Mutex
|
||||
bgpServer *bgp.Server
|
||||
|
||||
clientSet *kubernetes.Clientset
|
||||
rwClientSet *kubernetes.Clientset
|
||||
|
||||
shutdownChan chan struct{}
|
||||
|
||||
// This is a prometheus counter used to count the number of events received
|
||||
// from the service watcher
|
||||
CountServiceWatchEvent *prometheus.CounterVec
|
||||
|
||||
intfMgr *networkinterface.Manager
|
||||
arpMgr *arp.Manager
|
||||
|
||||
leaseMgr *lease.Manager
|
||||
|
||||
// nodeLabelManager is the manager for the node labels
|
||||
nodeLabelManager labelManager
|
||||
}
|
||||
|
||||
// labelManager is the interface for the node label manager to add/remove labels
|
||||
type labelManager interface {
|
||||
AddLabel(ctx context.Context, svc *v1.Service) error
|
||||
RemoveLabel(ctx context.Context, svc *v1.Service) error
|
||||
}
|
||||
|
||||
func NewServicesProcessor(config *kubevip.Config, bgpServer *bgp.Server,
|
||||
clientSet *kubernetes.Clientset, rwClientSet *kubernetes.Clientset, shutdownChan chan struct{},
|
||||
intfMgr *networkinterface.Manager, arpMgr *arp.Manager, nodeLabelManager labelManager) *Processor {
|
||||
lbClassFilterFunc := lbClassFilter
|
||||
if config.LoadBalancerClassLegacyHandling {
|
||||
lbClassFilterFunc = lbClassFilterLegacy
|
||||
}
|
||||
|
||||
return &Processor{
|
||||
config: config,
|
||||
lbClassFilter: lbClassFilterFunc,
|
||||
ServiceInstances: []*instance.Instance{},
|
||||
bgpServer: bgpServer,
|
||||
clientSet: clientSet,
|
||||
rwClientSet: rwClientSet,
|
||||
shutdownChan: shutdownChan,
|
||||
CountServiceWatchEvent: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "kube_vip",
|
||||
Subsystem: "manager",
|
||||
Name: "all_services_events",
|
||||
Help: "Count all events fired by the service watcher categorised by event type",
|
||||
}, []string{"type"}),
|
||||
|
||||
intfMgr: intfMgr,
|
||||
arpMgr: arpMgr,
|
||||
leaseMgr: lease.NewManager(),
|
||||
nodeLabelManager: nodeLabelManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) AddOrModify(ctx context.Context, event watch.Event, serviceFunc func(context.Context, *v1.Service) error) (bool, error) {
|
||||
// log.Debugf("Endpoints for service [%s] have been Created or modified", s.service.ServiceName)
|
||||
svc, ok := event.Object.(*v1.Service)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unable to parse Kubernetes services from API watcher")
|
||||
}
|
||||
|
||||
// We only care about LoadBalancer services
|
||||
if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check if we ignore this service
|
||||
if svc.Annotations[kubevip.LoadbalancerIgnore] == "true" {
|
||||
log.Info("ignore annotation for kube-vip", "service name", svc.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check the loadBalancer class
|
||||
if p.lbClassFilter(svc, p.config) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
svcAddresses, svcHostnames := instance.FetchServiceAddresses(svc)
|
||||
|
||||
// We only care about LoadBalancer services that have been allocated an address
|
||||
if len(svcAddresses) <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
_, usesCommonLease := svc.Annotations[kubevip.ServiceLease]
|
||||
if usesCommonLease && svc.Spec.ExternalTrafficPolicy != v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
return false, fmt.Errorf("annotation %q cannot be used with service traffic policy other than %q",
|
||||
kubevip.ServiceLease, v1.ServiceExternalTrafficPolicyTypeCluster)
|
||||
}
|
||||
|
||||
svcCtx, err := p.getServiceContext(svc.UID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get service context: %w", err)
|
||||
}
|
||||
|
||||
// The modified event should only be triggered if the service has been modified (i.e. moved somewhere else)
|
||||
if event.Type == watch.Modified {
|
||||
i := instance.FindServiceInstance(svc, p.ServiceInstances)
|
||||
shouldGarbageCollect := true
|
||||
if i != nil {
|
||||
originalServiceAddresses, originalServiceHostnames := instance.FetchServiceAddresses(i.ServiceSnapshot)
|
||||
shouldGarbageCollect =
|
||||
// Service addresses changed
|
||||
!reflect.DeepEqual(originalServiceAddresses, svcAddresses) ||
|
||||
// Service hostnames changed
|
||||
!reflect.DeepEqual(originalServiceHostnames, svcHostnames) ||
|
||||
// ExternalTrafficPolicy changed
|
||||
svc.Spec.ExternalTrafficPolicy != i.ServiceSnapshot.Spec.ExternalTrafficPolicy
|
||||
}
|
||||
if shouldGarbageCollect {
|
||||
for _, addr := range svcAddresses {
|
||||
// log.Debugf("(svcs) Retrieving local addresses, to ensure that this modified address doesn't exist: %s", addr)
|
||||
f, err := vip.GarbageCollect(p.config.Interface, addr, p.intfMgr)
|
||||
if err != nil {
|
||||
log.Error("(svcs) cleaning existing address error", "err", err)
|
||||
}
|
||||
if f {
|
||||
log.Warn("(svcs) already found existing config", "address", addr, "adapter", p.config.Interface)
|
||||
}
|
||||
}
|
||||
// This service has been modified, but it was also active.
|
||||
if svcCtx != nil && svcCtx.IsActive {
|
||||
log.Warn("(svcs) The load balancer has changed, cancelling original load balancer")
|
||||
//Set it to inactive
|
||||
svcCtx.IsActive = false
|
||||
svcCtx.Cancel()
|
||||
log.Warn("(svcs) waiting for load balancer to finish")
|
||||
<-svcCtx.Ctx.Done()
|
||||
|
||||
if err := p.deleteService(svc.UID); err != nil {
|
||||
log.Error("(svc) unable to remove", "service", svc.UID)
|
||||
}
|
||||
// in theory this should never fail
|
||||
p.svcMap.Delete(svc.UID)
|
||||
// Reset the the svcCtx when it was garbage collected
|
||||
// As the next function will create a new context when nil
|
||||
svcCtx = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Architecture walkthrough: (Had to do this as this code path is making my head hurt)
|
||||
|
||||
// Is the service active (bool), if not then process this new service
|
||||
// Does this service use an election per service?
|
||||
//
|
||||
|
||||
if svcCtx == nil || svcCtx != nil && !svcCtx.IsActive {
|
||||
ips, hostnames := instance.FetchServiceAddresses(svc)
|
||||
log.Debug("(svcs) has been added/modified with addresses", "service name", svc.Name, "ips", ips, "hostnames", hostnames)
|
||||
|
||||
if svcCtx == nil {
|
||||
svcCtx = servicecontext.New(ctx)
|
||||
p.svcMap.Store(svc.UID, svcCtx)
|
||||
}
|
||||
|
||||
if p.config.EnableServicesElection || // Service Election
|
||||
((p.config.EnableRoutingTable || p.config.EnableBGP) && // Routing table mode or BGP
|
||||
(!p.config.EnableLeaderElection && !p.config.EnableServicesElection)) { // No leaderelection or services election
|
||||
|
||||
// If this load balancer Traffic Policy is "local"
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
|
||||
|
||||
// Start an endpoint watcher if we're not watching it already
|
||||
if !svcCtx.IsWatched {
|
||||
// background the endpoint watcher
|
||||
if (p.config.EnableRoutingTable || p.config.EnableBGP) && (!p.config.EnableLeaderElection && !p.config.EnableServicesElection) {
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
|
||||
// Add Endpoint or EndpointSlices watcher
|
||||
var provider providers.Provider
|
||||
if p.config.EnableEndpoints {
|
||||
provider = providers.NewEndpoints()
|
||||
} else {
|
||||
provider = providers.NewEndpointslices()
|
||||
}
|
||||
if err = p.watchEndpoint(svcCtx, p.config.NodeName, svc, provider); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// We're now watching this service
|
||||
svcCtx.IsWatched = true
|
||||
}
|
||||
} else if (p.config.EnableBGP || p.config.EnableRoutingTable) && (!p.config.EnableLeaderElection && !p.config.EnableServicesElection) {
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
// Add Endpoint watcher
|
||||
var provider providers.Provider
|
||||
if p.config.EnableEndpoints {
|
||||
provider = providers.NewEndpoints()
|
||||
} else {
|
||||
provider = providers.NewEndpointslices()
|
||||
}
|
||||
if err = p.watchEndpoint(svcCtx, p.config.NodeName, svc, provider); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
// We're now watching this service
|
||||
svcCtx.IsWatched = true
|
||||
} else {
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-svcCtx.Ctx.Done():
|
||||
log.Warn("(svcs) restartable service watcher ending", "uid", svc.UID)
|
||||
return
|
||||
default:
|
||||
log.Info("(svcs) restartable service watcher starting", "uid", svc.UID)
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
// Increment the waitGroup before the service Func is called (Done is completed in there)
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
svcCtx.IsActive = true
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *Processor) Delete(event watch.Event) (bool, error) {
|
||||
svc, ok := event.Object.(*v1.Service)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("(svcs) unable to parse Kubernetes services from API watcher")
|
||||
}
|
||||
svcCtx, err := p.getServiceContext(svc.UID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("(svcs) unable to get context: %w", err)
|
||||
}
|
||||
|
||||
if svcCtx != nil {
|
||||
// We only care about LoadBalancer services
|
||||
if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// We can ignore this service
|
||||
if svc.Annotations[kubevip.LoadbalancerIgnore] == "true" {
|
||||
log.Info("(svcs) ignore annotation for kube-vip", "service name", svc.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If no leader election is enabled, delete routes here
|
||||
if !p.config.EnableLeaderElection && !p.config.EnableServicesElection &&
|
||||
p.config.EnableRoutingTable && svcCtx.HasConfiguredNetworks() {
|
||||
if errs := endpoints.ClearRoutes(svc, &p.ServiceInstances); len(errs) == 0 {
|
||||
svcCtx.ConfiguredNetworks.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an active service then and additional leaderElection will handle stopping
|
||||
err = p.deleteService(svc.UID)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
|
||||
// Calls the cancel function of the context
|
||||
log.Warn("(svcs) The load balancer was deleted, cancelling context")
|
||||
svcCtx.IsActive = false
|
||||
svcCtx.Cancel()
|
||||
log.Warn("(svcs) waiting for load balancer to finish")
|
||||
<-svcCtx.Ctx.Done()
|
||||
p.svcMap.Delete(svc.UID)
|
||||
}
|
||||
|
||||
log.Info("(svcs) deleted", "service name", svc.Name, "namespace", svc.Namespace)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *Processor) Stop() {
|
||||
for _, instance := range p.ServiceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) getServiceContext(uid types.UID) (*servicecontext.Context, error) {
|
||||
svcCtx, ok := p.svcMap.Load(uid)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, ok := svcCtx.(*servicecontext.Context)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cast service context pointer - UID: %s", uid)
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (p *Processor) CountRouteReferences(route *netlink.Route) int {
|
||||
return endpoints.CountRouteReferences(route, &p.ServiceInstances)
|
||||
}
|
||||
550
pkg/services/services.go
Normal file
550
pkg/services/services.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/egress"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/upnp"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
type ServiceInstanceAction string
|
||||
|
||||
const (
|
||||
ActionDelete ServiceInstanceAction = "delete"
|
||||
ActionAdd ServiceInstanceAction = "add"
|
||||
ActionNone ServiceInstanceAction = "none"
|
||||
)
|
||||
|
||||
func (p *Processor) SyncServices(ctx context.Context, svc *v1.Service) error {
|
||||
log.Debug("[STARTING] Service Sync", "namespace", svc.Namespace, "name", svc.Name)
|
||||
|
||||
// Iterate through the synchronising services
|
||||
|
||||
action := p.getServiceInstanceAction(svc)
|
||||
switch action {
|
||||
case ActionDelete:
|
||||
// remove the label from the node before deleting the service
|
||||
if err := p.nodeLabelManager.RemoveLabel(ctx, svc); err != nil {
|
||||
return fmt.Errorf("error removing label from node: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("[service] delete", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
if err := p.deleteService(svc.UID); err != nil {
|
||||
return fmt.Errorf("error deleting service %s/%s: %w", svc.Namespace, svc.Name, err)
|
||||
}
|
||||
case ActionAdd:
|
||||
log.Debug("[service] add", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
if err := p.addService(ctx, svc); err != nil {
|
||||
return fmt.Errorf("error adding service %s/%s: %w", svc.Namespace, svc.Name, err)
|
||||
}
|
||||
|
||||
// add the label to the node after adding the service
|
||||
if err := p.nodeLabelManager.AddLabel(ctx, svc); err != nil {
|
||||
return fmt.Errorf("error adding label to node: %w", err)
|
||||
}
|
||||
case ActionNone:
|
||||
log.Debug("[service] no action", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
}
|
||||
log.Debug("[FINISHED] Service Sync", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) getServiceInstanceAction(svc *v1.Service) ServiceInstanceAction {
|
||||
// protect against multiple calls
|
||||
// get the annotations or legacy values from manual configuration
|
||||
addresses, hostnames := instance.FetchServiceAddresses(svc)
|
||||
// get the status information of the LB Service
|
||||
statusAddresses, _ := instance.FetchLoadBalancerIngress(svc)
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
for _, instance := range p.ServiceInstances {
|
||||
if instance != nil && instance.ServiceSnapshot.UID == svc.UID {
|
||||
for _, address := range addresses {
|
||||
// handle the case where the service instance needs to be deleted
|
||||
if instance.IsDHCP {
|
||||
if address != "0.0.0.0" && address != "::" {
|
||||
return ActionDelete
|
||||
}
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, instance.DHCPInterfaceIP) {
|
||||
return ActionDelete
|
||||
}
|
||||
}
|
||||
if !instance.IsDHCP {
|
||||
if address == "0.0.0.0" || address == "::" {
|
||||
return ActionDelete
|
||||
}
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, address) {
|
||||
return ActionDelete
|
||||
}
|
||||
}
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !comparePortsAndPortStatuses(svc) {
|
||||
return ActionDelete
|
||||
}
|
||||
}
|
||||
// If we reach here, it means the service instance matches the service UID and is not a DHCP service, so we can return "no action"
|
||||
return ActionNone
|
||||
}
|
||||
}
|
||||
if len(addresses) > 0 || len(hostnames) > 0 {
|
||||
log.Debug("no matching service instance found", "service", svc.Name, "namespace", svc.Namespace, "addresses", addresses, "hostnames", hostnames)
|
||||
return ActionAdd // If no matching instance is found, we need to add a new service instance
|
||||
}
|
||||
return ActionNone
|
||||
}
|
||||
|
||||
func comparePortsAndPortStatuses(svc *v1.Service) bool {
|
||||
if len(svc.Status.LoadBalancer.Ingress) == 0 {
|
||||
return false
|
||||
}
|
||||
portsStatus := svc.Status.LoadBalancer.Ingress[0].Ports
|
||||
if len(portsStatus) != len(svc.Spec.Ports) {
|
||||
return false
|
||||
}
|
||||
for i, portSpec := range svc.Spec.Ports {
|
||||
if portsStatus[i].Port != portSpec.Port || portsStatus[i].Protocol != portSpec.Protocol {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Processor) addService(ctx context.Context, svc *v1.Service) error {
|
||||
// protect against addService while reading
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
newService, err := instance.NewInstance(svc, p.config, p.intfMgr, p.arpMgr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for x := range newService.VIPConfigs {
|
||||
log.Debug("starting loadbalancer for service", "name", svc.Name, "namespace", svc.Namespace)
|
||||
newService.Clusters[x].StartLoadBalancerService(ctx, newService.VIPConfigs[x], p.bgpServer, svc.Name, p.CountRouteReferences)
|
||||
}
|
||||
|
||||
p.upnpMap(ctx, newService)
|
||||
|
||||
if newService.IsDHCP && len(newService.VIPConfigs) == 1 {
|
||||
go func() {
|
||||
for ip := range newService.DHCPClient.IPChannel() {
|
||||
log.Debug("IP changed", "ip", ip)
|
||||
newService.VIPConfigs[0].VIP = ip
|
||||
newService.DHCPInterfaceIP = ip
|
||||
if !p.config.DisableServiceUpdates {
|
||||
if err := p.updateStatus(newService); err != nil {
|
||||
log.Warn("updating svc", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debug("IP update channel closed, stopping")
|
||||
}()
|
||||
}
|
||||
|
||||
p.ServiceInstances = append(p.ServiceInstances, newService)
|
||||
|
||||
if !p.config.DisableServiceUpdates {
|
||||
log.Debug("[service] update", "namespace", newService.ServiceSnapshot.Namespace, "name", newService.ServiceSnapshot.Name)
|
||||
if err := p.updateStatus(newService); err != nil {
|
||||
log.Error("[service] updating status", "namespace", newService.ServiceSnapshot.Namespace, "name", newService.ServiceSnapshot.Name, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
serviceIPs, _ := instance.FetchServiceAddresses(svc)
|
||||
// Check if we need to flush any conntrack connections (due to some dangling conntrack connections)
|
||||
if svc.Annotations[kubevip.FlushContrack] == "true" {
|
||||
|
||||
log.Debug("[service] Flushing conntrack rules", "service", svc.Name, "namespace", svc.Namespace)
|
||||
for _, serviceIP := range serviceIPs {
|
||||
err = vip.DeleteExistingSessions(serviceIP, false, svc.Annotations[kubevip.EgressDestinationPorts], svc.Annotations[kubevip.EgressSourcePorts])
|
||||
if err != nil {
|
||||
log.Error("[service] flushing any remaining egress connections", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
err = vip.DeleteExistingSessions(serviceIP, true, svc.Annotations[kubevip.EgressDestinationPorts], svc.Annotations[kubevip.EgressSourcePorts])
|
||||
if err != nil {
|
||||
log.Error("[service] flushing any remaining ingress connections", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if egress is enabled on the service, if so we'll need to configure some rules
|
||||
if svc.Annotations[kubevip.Egress] == "true" && len(serviceIPs) > 0 {
|
||||
log.Debug("[service] enabling egress", "service", svc.Name, "namespace", svc.Namespace)
|
||||
// If we'er not using NFtables, then ensure that the correct iptables modules are loaded
|
||||
if p.config.EgressWithNftables {
|
||||
// Ensure that kernel modules are loaded and report back missing modules.
|
||||
err = p.nftablesCheck()
|
||||
if err != nil {
|
||||
log.Warn("[service] configuring nft egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
} else {
|
||||
// Ensure that kernel modules are loaded and report back missing modules.
|
||||
err = p.iptablesCheck()
|
||||
if err != nil {
|
||||
log.Warn("[service] configuring egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
var podIP string
|
||||
errList := []error{}
|
||||
|
||||
// Should egress be IPv6
|
||||
if svc.Annotations[kubevip.EgressIPv6] == "true" {
|
||||
// Does the service have an active IPv6 endpoint
|
||||
if svc.Annotations[kubevip.ActiveEndpointIPv6] != "" {
|
||||
for _, serviceIP := range serviceIPs {
|
||||
if !p.config.EnableEndpoints && utils.IsIPv6(serviceIP) {
|
||||
|
||||
podIP = svc.Annotations[kubevip.ActiveEndpointIPv6]
|
||||
|
||||
err = p.configureEgress(serviceIP, podIP, svc.Namespace, string(svc.UID), svc.Annotations)
|
||||
if err != nil {
|
||||
errList = append(errList, err)
|
||||
log.Warn("[service] configuring egress IPv6", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if svc.Annotations[kubevip.ActiveEndpoint] != "" { // Not expected to be IPv6, so should be an IPv4 address
|
||||
for _, serviceIP := range serviceIPs {
|
||||
podIPs := svc.Annotations[kubevip.ActiveEndpoint]
|
||||
if !p.config.EnableEndpoints && utils.IsIPv6(serviceIP) {
|
||||
podIPs = svc.Annotations[kubevip.ActiveEndpointIPv6]
|
||||
}
|
||||
err = p.configureEgress(serviceIP, podIPs, svc.Namespace, string(svc.UID), svc.Annotations)
|
||||
if err != nil {
|
||||
errList = append(errList, err)
|
||||
log.Warn("[service] configuring egress IPv4", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(errList) == 0 {
|
||||
var provider providers.Provider
|
||||
if p.config.EnableEndpoints {
|
||||
provider = providers.NewEndpoints()
|
||||
} else {
|
||||
provider = providers.NewEndpointslices()
|
||||
}
|
||||
err = provider.UpdateServiceAnnotation(svc.Annotations[kubevip.ActiveEndpoint], svc.Annotations[kubevip.ActiveEndpointIPv6], svc, p.clientSet)
|
||||
if err != nil {
|
||||
log.Warn("[service] configuring egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finishTime := time.Since(startTime)
|
||||
log.Info("[service]", "service", svc.Name, "namespace", svc.Namespace, "synchronised in", fmt.Sprintf("%dms", finishTime.Milliseconds()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) deleteService(uid types.UID) error {
|
||||
// protect multiple calls
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
var updatedInstances []*instance.Instance
|
||||
var serviceInstance *instance.Instance
|
||||
found := false
|
||||
for x := range p.ServiceInstances {
|
||||
log.Debug("[service] lookup", "target UID", uid, "found UID ", p.ServiceInstances[x].ServiceSnapshot.UID, "name", p.ServiceInstances[x].ServiceSnapshot.Name, "namespace", p.ServiceInstances[x].ServiceSnapshot.Namespace)
|
||||
// Add the running services to the new array
|
||||
if p.ServiceInstances[x].ServiceSnapshot.UID != uid {
|
||||
updatedInstances = append(updatedInstances, p.ServiceInstances[x])
|
||||
} else {
|
||||
// Flip the found when we match
|
||||
found = true
|
||||
serviceInstance = p.ServiceInstances[x]
|
||||
}
|
||||
}
|
||||
|
||||
// If we've been through all services and not found the correct one then error
|
||||
if !found {
|
||||
// TODO: - fix UX
|
||||
// return fmt.Errorf("unable to find/stop service [%s]", uid)
|
||||
log.Warn("unable to find/stop service", "uid", uid)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, c := range serviceInstance.Clusters {
|
||||
for n := range c.Network {
|
||||
c.Network[n].SetHasEndpoints(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this this VIP is shared with other loadbalancers
|
||||
shared := false
|
||||
vipSet := make(map[string]interface{})
|
||||
for x := range updatedInstances {
|
||||
vips, _ := instance.FetchServiceAddresses(updatedInstances[x].ServiceSnapshot)
|
||||
for _, vip := range vips { //updatedInstances[x].ServiceSnapshot.Spec.LoadBalancerIP {
|
||||
vipSet[vip] = nil
|
||||
}
|
||||
}
|
||||
vips, _ := instance.FetchServiceAddresses(serviceInstance.ServiceSnapshot)
|
||||
for _, vip := range vips {
|
||||
if _, found := vipSet[vip]; found {
|
||||
shared = true
|
||||
}
|
||||
}
|
||||
if !shared {
|
||||
for x := range serviceInstance.Clusters {
|
||||
serviceInstance.Clusters[x].Stop()
|
||||
}
|
||||
if serviceInstance.IsDHCP {
|
||||
serviceInstance.DHCPClient.Stop()
|
||||
macvlan, err := netlink.LinkByName(serviceInstance.DHCPInterface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[service] error finding VIP Interface: %v", err)
|
||||
}
|
||||
|
||||
err = netlink.LinkDel(macvlan)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[service] error deleting DHCP Link : %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if p.config.EnableBGP {
|
||||
endpoints.ClearBGPHostsByInstance(serviceInstance, p.bgpServer)
|
||||
}
|
||||
if p.config.EnableRoutingTable && (p.config.EnableLeaderElection || p.config.EnableServicesElection) {
|
||||
if errs := endpoints.ClearRoutesByInstance(serviceInstance.ServiceSnapshot, serviceInstance, &p.ServiceInstances); len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Error("unable to clear routes", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We will need to tear down the egress
|
||||
if serviceInstance.ServiceSnapshot.Annotations[kubevip.Egress] == "true" {
|
||||
if serviceInstance.ServiceSnapshot.Annotations[kubevip.ActiveEndpoint] != "" {
|
||||
log.Info("[service] egress re-write enabled", "service", serviceInstance.ServiceSnapshot.Name)
|
||||
err := egress.Teardown(serviceInstance.ServiceSnapshot.Annotations[kubevip.ActiveEndpoint], serviceInstance.ServiceSnapshot.Spec.LoadBalancerIP, serviceInstance.ServiceSnapshot.Namespace, string(serviceInstance.ServiceSnapshot.UID), serviceInstance.ServiceSnapshot.Annotations, p.config.EgressWithNftables)
|
||||
if err != nil {
|
||||
log.Error("[service] egress teardown", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the service array
|
||||
p.ServiceInstances = updatedInstances
|
||||
|
||||
log.Info("Removed instance from manager", "uid", uid, "name", serviceInstance.ServiceSnapshot.Name, "remaining advertised services", len(p.ServiceInstances))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set up UPNP forwards for a service
|
||||
// We first try to use the more modern Pinhole API introduced in UPNPv2 and fall back to UPNPv2 Port Forwarding if no forward was successful
|
||||
func (p *Processor) upnpMap(ctx context.Context, s *instance.Instance) {
|
||||
if !isUPNPEnabled(s.ServiceSnapshot) {
|
||||
// Skip services missing the annotation
|
||||
return
|
||||
}
|
||||
if !p.config.EnableUPNP {
|
||||
log.Warn("[UPNP] Found kube-vip.io/forwardUPNP on service while UPNP forwarding is disabled in the kube-vip config. Not forwarding", "service", s.ServiceSnapshot.Name)
|
||||
return
|
||||
}
|
||||
// If upnp is enabled then update the gateway/router with the address
|
||||
// TODO - check if this implementation for dualstack is correct
|
||||
|
||||
gateways := upnp.GetGatewayClients(ctx)
|
||||
|
||||
// Reset Gateway IPs to remove stale addresses
|
||||
s.UPNPGatewayIPs = make([]string, 0)
|
||||
|
||||
vips, _ := instance.FetchServiceAddresses(s.ServiceSnapshot)
|
||||
for _, vip := range vips {
|
||||
for _, port := range s.ServiceSnapshot.Spec.Ports {
|
||||
for _, gw := range gateways {
|
||||
|
||||
forwardSucessful := false
|
||||
if gw.WANIPv6FirewallControlClient != nil {
|
||||
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name, "gateway", gw.WANIPv6FirewallControlClient.Location)
|
||||
|
||||
pinholeID, pinholeErr := gw.WANIPv6FirewallControlClient.AddPinholeCtx(ctx, "0.0.0.0", uint16(port.Port), vip, uint16(port.Port), upnp.MapProtocolToIANA(string(port.Protocol)), 3600) //nolint TODO
|
||||
if pinholeErr == nil {
|
||||
forwardSucessful = true
|
||||
log.Info("[UPNP] Service should be accessible externally", "port", port.Port, "pinhold ID", pinholeID)
|
||||
} else {
|
||||
//TODO: Cleanup
|
||||
log.Error("[UPNP] Unable to map port to gateway using Pinhole API", "err", pinholeErr.Error())
|
||||
}
|
||||
}
|
||||
// Fallback to PortForward
|
||||
if !forwardSucessful {
|
||||
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name)
|
||||
|
||||
portMappingErr := gw.ConnectionClient.AddPortMapping("0.0.0.0", uint16(port.Port), strings.ToUpper(string(port.Protocol)), uint16(port.Port), vip, true, s.ServiceSnapshot.Name, 3600) //nolint TODO
|
||||
if portMappingErr == nil {
|
||||
ip, err := gw.ConnectionClient.GetExternalIPAddress()
|
||||
if err != nil {
|
||||
// Log the error but continue on the off chance the mapping was successful
|
||||
log.Error("[UPNP] Unable to get external IP address from gateway", "port", port.Port, "err", err)
|
||||
} else {
|
||||
log.Info("[UPNP] Service should be accessible externally", "port", port.Port, "externalip", ip)
|
||||
}
|
||||
forwardSucessful = true
|
||||
} else {
|
||||
//TODO: Cleanup
|
||||
log.Error("[UPNP] Unable to map port to gateway using PortForward API", "err", portMappingErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if forwardSucessful {
|
||||
ip, err := gw.ConnectionClient.GetExternalIPAddress()
|
||||
if err == nil {
|
||||
s.UPNPGatewayIPs = append(s.UPNPGatewayIPs, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicate IPs
|
||||
slices.Sort(s.UPNPGatewayIPs)
|
||||
s.UPNPGatewayIPs = slices.Compact(s.UPNPGatewayIPs)
|
||||
}
|
||||
|
||||
func (p *Processor) updateStatus(i *instance.Instance) error {
|
||||
// let's retry status update every 10ms for 30s
|
||||
retryConfig := wait.Backoff{
|
||||
Steps: 3000,
|
||||
Duration: 10 * time.Millisecond,
|
||||
Factor: 0,
|
||||
Jitter: 0.1,
|
||||
}
|
||||
// will retry for every error encountered, TODO: should a list of errors that will trigger retry be specified?
|
||||
err := retry.OnError(retryConfig, func(error) bool { return true }, func() error {
|
||||
// Retrieve the latest version of Deployment before attempting update
|
||||
// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
|
||||
currentService, err := p.clientSet.CoreV1().Services(i.ServiceSnapshot.Namespace).Get(context.TODO(), i.ServiceSnapshot.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentServiceCopy := currentService.DeepCopy()
|
||||
if currentServiceCopy.Annotations == nil {
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
// If we're using ARP then we can only broadcast the VIP from one place, also useful for other software when running BGP, add an annotation to the service
|
||||
if p.config.EnableARP || p.config.EnableBGP {
|
||||
// Add the current host
|
||||
currentServiceCopy.Annotations[kubevip.VipHost] = p.config.NodeName
|
||||
}
|
||||
if i.DHCPInterfaceHwaddr != "" || i.DHCPInterfaceIP != "" {
|
||||
currentServiceCopy.Annotations[kubevip.HwAddrKey] = i.DHCPInterfaceHwaddr
|
||||
currentServiceCopy.Annotations[kubevip.RequestedIP] = i.DHCPInterfaceIP
|
||||
}
|
||||
|
||||
if currentService.Annotations["development.kube-vip.io/synthetic-api-server-error-on-update"] == "true" {
|
||||
log.Error("(Synthetic error ) updating Spec", "service", i.ServiceSnapshot.Name, "err", err)
|
||||
return fmt.Errorf("(Synthetic) simulating api server errors")
|
||||
}
|
||||
|
||||
if !cmp.Equal(currentService, currentServiceCopy) {
|
||||
currentService, err = p.clientSet.CoreV1().Services(currentServiceCopy.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Error("updating Spec", "service", i.ServiceSnapshot.Name, "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ports := make([]v1.PortStatus, 0, len(i.ServiceSnapshot.Spec.Ports))
|
||||
for _, port := range i.ServiceSnapshot.Spec.Ports {
|
||||
ports = append(ports, v1.PortStatus{
|
||||
Port: port.Port,
|
||||
Protocol: port.Protocol,
|
||||
})
|
||||
}
|
||||
|
||||
ingresses := []v1.LoadBalancerIngress{}
|
||||
|
||||
for _, c := range i.VIPConfigs {
|
||||
if !utils.IsIP(c.VIP) {
|
||||
ips, err := utils.LookupHost(c.VIP, p.config.DNSMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ip := range ips {
|
||||
i := v1.LoadBalancerIngress{
|
||||
IP: ip,
|
||||
Ports: ports,
|
||||
}
|
||||
ingresses = append(ingresses, i)
|
||||
}
|
||||
} else {
|
||||
i := v1.LoadBalancerIngress{
|
||||
IP: c.VIP,
|
||||
Ports: ports,
|
||||
}
|
||||
ingresses = append(ingresses, i)
|
||||
}
|
||||
if isUPNPEnabled(currentService) {
|
||||
for _, ip := range i.UPNPGatewayIPs {
|
||||
i := v1.LoadBalancerIngress{
|
||||
IP: ip,
|
||||
Ports: ports,
|
||||
}
|
||||
ingresses = append(ingresses, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cmp.Equal(currentService.Status.LoadBalancer.Ingress, ingresses) {
|
||||
currentService.Status.LoadBalancer.Ingress = ingresses
|
||||
_, err = p.clientSet.CoreV1().Services(currentService.Namespace).UpdateStatus(context.TODO(), currentService, metav1.UpdateOptions{})
|
||||
if err != nil && !apierrors.IsInvalid(err) {
|
||||
log.Error("updating Service", "namespace", i.ServiceSnapshot.Namespace, "name", i.ServiceSnapshot.Name, "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func isUPNPEnabled(s *v1.Service) bool {
|
||||
return metav1.HasAnnotation(s.ObjectMeta, kubevip.UpnpEnabled) && s.Annotations[kubevip.UpnpEnabled] == "true"
|
||||
}
|
||||
|
||||
// Refresh UPNP Port Forwards for all Service Instances registered in the processor
|
||||
func (p *Processor) RefreshUPNPForwards() {
|
||||
log.Info("Starting UPNP Port Refresher")
|
||||
for {
|
||||
time.Sleep(300 * time.Second)
|
||||
|
||||
log.Info("[UPNP] Refreshing Instances", "number of instances", len(p.ServiceInstances))
|
||||
for i := range p.ServiceInstances {
|
||||
p.upnpMap(context.TODO(), p.ServiceInstances[i])
|
||||
if err := p.updateStatus(p.ServiceInstances[i]); err != nil {
|
||||
log.Warn("[UPNP] Error updating service", "ip", p.ServiceInstances[i].ServiceSnapshot.Name, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
pkg/services/watch_endpoints.go
Normal file
94
pkg/services/watch_endpoints.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
func (p *Processor) watchEndpoint(svcCtx *servicecontext.Context, id string, service *v1.Service, provider providers.Provider) error {
|
||||
log.Info("watching", "provider", provider.GetLabel(), "service_name", service.Name, "namespace", service.Namespace)
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
|
||||
leaderCtx, cancel := context.WithCancel(svcCtx.Ctx)
|
||||
defer cancel()
|
||||
|
||||
var leaderElectionActive bool
|
||||
|
||||
rw, err := provider.CreateRetryWatcher(leaderCtx, p.rwClientSet, service)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error watching endpoints: %w", provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
exitFunction := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-svcCtx.Ctx.Done():
|
||||
log.Debug("context cancelled", "provider", provider.GetLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
case <-p.shutdownChan:
|
||||
log.Debug("shutdown called", "provider", provider.GetLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
case <-exitFunction:
|
||||
log.Debug("function ending", "provider", provider.GetLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ch := rw.ResultChan()
|
||||
|
||||
epProcessor := endpoints.NewEndpointProcessor(p.config, provider, p.bgpServer, &p.ServiceInstances)
|
||||
|
||||
var lastKnownGoodEndpoint string
|
||||
for event := range ch {
|
||||
// We need to inspect the event and get ResourceVersion out of it
|
||||
switch event.Type {
|
||||
|
||||
case watch.Added, watch.Modified:
|
||||
restart, err := epProcessor.AddOrModify(svcCtx, event, &lastKnownGoodEndpoint, service, id, &leaderElectionActive, p.StartServicesLeaderElection, &leaderCtx, &cancel)
|
||||
if restart {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("[%s] error while processing add/modify event: %w", provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
case watch.Deleted:
|
||||
if err := epProcessor.Delete(service, id); err != nil {
|
||||
return fmt.Errorf("[%s] error while processing delete event: %w", provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
// Close the goroutine that will end the retry watcher, then exit the endpoint watcher function
|
||||
close(exitFunction)
|
||||
log.Info("stopping watching", "provider", provider.GetLabel(), "service name", service.Name, "namespace", service.Namespace)
|
||||
|
||||
return nil
|
||||
case watch.Error:
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, _ := errObject.(*apierrors.StatusError)
|
||||
log.Error("watch error", "provider", provider.GetLabel(), "err", statusErr)
|
||||
}
|
||||
}
|
||||
close(exitFunction)
|
||||
log.Info("stopping watching", "provider", provider.GetLabel(), "service name", service.Name, "namespace", service.Namespace)
|
||||
return nil //nolint:govet
|
||||
}
|
||||
183
pkg/services/watch_services.go
Normal file
183
pkg/services/watch_services.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/trafficmirror"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
// This function handles the watching of a services endpoints and updates a load balancers endpoint configurations accordingly
|
||||
func (p *Processor) ServicesWatcher(ctx context.Context, serviceFunc func(context.Context, *v1.Service) error) error {
|
||||
// first start port mirroring if enabled
|
||||
if err := p.startTrafficMirroringIfEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
// clean up traffic mirror related config
|
||||
err := p.stopTrafficMirroringIfEnabled()
|
||||
if err != nil {
|
||||
log.Error("Stopping traffic mirroring", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if p.config.ServiceNamespace == "" {
|
||||
// v1.NamespaceAll is actually "", but we'll stay with the const in case things change upstream
|
||||
p.config.ServiceNamespace = v1.NamespaceAll
|
||||
log.Info("(svcs) starting services watcher for all namespaces")
|
||||
} else {
|
||||
log.Info("(svcs) starting services watcher", "namespace", p.config.ServiceNamespace)
|
||||
}
|
||||
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctx, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return p.rwClientSet.CoreV1().Services(p.config.ServiceNamespace).Watch(ctx, metav1.ListOptions{})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating services watcher: %s", err.Error())
|
||||
}
|
||||
exitFunction := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-p.shutdownChan:
|
||||
log.Debug("(svcs) shutdown called")
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
return
|
||||
case <-exitFunction:
|
||||
log.Debug("(svcs) function ending")
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
return
|
||||
}
|
||||
}()
|
||||
ch := rw.ResultChan()
|
||||
|
||||
// Used for tracking an active endpoint / pod
|
||||
for event := range ch {
|
||||
p.CountServiceWatchEvent.With(prometheus.Labels{"type": string(event.Type)}).Add(1)
|
||||
|
||||
// We need to inspect the event and get ResourceVersion out of it
|
||||
switch event.Type {
|
||||
case watch.Added, watch.Modified:
|
||||
restart, err := p.AddOrModify(ctx, event, serviceFunc)
|
||||
if restart {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("add/modify service error: %w", err)
|
||||
}
|
||||
case watch.Deleted:
|
||||
restart, err := p.Delete(event)
|
||||
if restart {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete service error: %w", err)
|
||||
}
|
||||
case watch.Bookmark:
|
||||
// Un-used
|
||||
case watch.Error:
|
||||
log.Error("Error attempting to watch Kubernetes services")
|
||||
|
||||
// This round trip allows us to handle unstructured status
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, ok := errObject.(*apierrors.StatusError)
|
||||
if !ok {
|
||||
log.Error(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
|
||||
}
|
||||
|
||||
status := statusErr.ErrStatus
|
||||
log.Error("services", "err", status)
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(exitFunction)
|
||||
log.Warn("Stopping watching services for type: LoadBalancer in all namespaces")
|
||||
return nil
|
||||
}
|
||||
|
||||
func lbClassFilterLegacy(svc *v1.Service, config *kubevip.Config) bool {
|
||||
if svc == nil {
|
||||
log.Info("(svcs) service is nil, ignoring")
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass != nil {
|
||||
// if this isn't nil then it has been configured, check if it the kube-vip loadBalancer class
|
||||
if *svc.Spec.LoadBalancerClass != config.LoadBalancerClassName {
|
||||
log.Info("(svcs) specified the wrong loadBalancer class", "service name", svc.Name, "lbClass", *svc.Spec.LoadBalancerClass)
|
||||
return true
|
||||
}
|
||||
} else if config.LoadBalancerClassOnly {
|
||||
// if kube-vip is configured to only recognize services with kube-vip's lb class, then ignore the services without any lb class
|
||||
log.Info("(svcs) kube-vip configured to only recognize services with kube-vip's lb class but the service didn't specify any loadBalancer class, ignoring", "service name", svc.Name)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func lbClassFilter(svc *v1.Service, config *kubevip.Config) bool {
|
||||
if svc == nil {
|
||||
log.Info("(svcs) service is nil, ignoring")
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass == nil && config.LoadBalancerClassName != "" {
|
||||
log.Info("(svcs) no loadBalancer class, ignoring", "service name", svc.Name, "expected lbClass", config.LoadBalancerClassName)
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass == nil && config.LoadBalancerClassName == "" {
|
||||
return false
|
||||
}
|
||||
if *svc.Spec.LoadBalancerClass != config.LoadBalancerClassName {
|
||||
log.Info("(svcs) specified wrong loadBalancer class, ignoring", "service name", svc.Name, "wrong lbClass", *svc.Spec.LoadBalancerClass, "expected lbClass", config.LoadBalancerClassName)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Processor) serviceInterface() string {
|
||||
svcIf := p.config.Interface
|
||||
if p.config.ServicesInterface != "" {
|
||||
svcIf = p.config.ServicesInterface
|
||||
}
|
||||
return svcIf
|
||||
}
|
||||
|
||||
func (p *Processor) startTrafficMirroringIfEnabled() error {
|
||||
if p.config.MirrorDestInterface != "" {
|
||||
svcIf := p.serviceInterface()
|
||||
log.Info("mirroring traffic", "src", svcIf, "dest", p.config.MirrorDestInterface)
|
||||
if err := trafficmirror.MirrorTrafficFromNIC(svcIf, p.config.MirrorDestInterface); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("skip starting traffic mirroring since it's not enabled.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) stopTrafficMirroringIfEnabled() error {
|
||||
if p.config.MirrorDestInterface != "" {
|
||||
svcIf := p.serviceInterface()
|
||||
log.Info("clean up qdisc config", "interface", svcIf)
|
||||
if err := trafficmirror.CleanupQDSICFromNIC(svcIf); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("skip stopping traffic mirroring since it's not enabled.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
74
pkg/sysctl/sysctl.go
Normal file
74
pkg/sysctl/sysctl.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package sysctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func WriteProcSys(path, value string) error {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if cErr := f.Close(); cErr != nil && err == nil {
|
||||
err = fmt.Errorf("failed to close file: %w", cErr)
|
||||
}
|
||||
}()
|
||||
|
||||
n, err := f.Write([]byte(value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write value: %w", err)
|
||||
}
|
||||
if n < len(value) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckProcSys(path string) (bool, error) {
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if cErr := f.Close(); cErr != nil && err == nil {
|
||||
err = fmt.Errorf("failed to close file: %w", cErr)
|
||||
}
|
||||
}()
|
||||
|
||||
buffer := make([]byte, 1)
|
||||
|
||||
if n, err := f.Read(buffer); err != nil || n < len(buffer) {
|
||||
return false, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
var isEnabled bool
|
||||
isEnabled, err = strconv.ParseBool(string(buffer))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to parse value: %w", err)
|
||||
}
|
||||
|
||||
return isEnabled, nil
|
||||
}
|
||||
|
||||
func EnableProcSys(path string) (bool, error) {
|
||||
isEnabled, err := CheckProcSys(path)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check '%s' status: %w", path, err)
|
||||
}
|
||||
if !isEnabled {
|
||||
if err := WriteProcSys(path, "1"); err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return false, fmt.Errorf("no permission to write to the file '%s' - please ensure that kube-vip is running with proper capabilities/privileged mode to write to sysfs: %w", path, err)
|
||||
}
|
||||
return false, fmt.Errorf("failed to enable '%s': %w", path, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
190
pkg/trafficmirror/trafficmirror.go
Normal file
190
pkg/trafficmirror/trafficmirror.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package trafficmirror
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var errQdiscNotFound = errors.New("qdisc not found")
|
||||
|
||||
// MirrorTrafficFromNIC use netlink to implement tc command to mirror traffic from
|
||||
// one interface to another
|
||||
func MirrorTrafficFromNIC(fromNICName, toNICName string) error {
|
||||
// name of nic which traffic will be mirrored from
|
||||
fromNIC, err := netlink.LinkByName(fromNICName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find nic %s: %v", fromNICName, err)
|
||||
}
|
||||
fromNICID := fromNIC.Attrs().Index
|
||||
|
||||
// name of nic which traffic will be mirrored to
|
||||
toNIC, err := netlink.LinkByName(toNICName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find nic %s: %v", toNICName, err)
|
||||
}
|
||||
toNICID := toNIC.Attrs().Index
|
||||
log.Debug(fmt.Sprintf("interface %s has index %d", fromNICName, fromNICID))
|
||||
log.Debug(fmt.Sprintf("interface %s has index %d", toNICName, toNICID))
|
||||
|
||||
log.Debug(fmt.Sprintf("clean up interface %s first in case it has stale qdsic", fromNICName))
|
||||
if err := CleanupQDSICFromNIC(fromNICName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("step 1: tc qdisc add dev %s ingress", fromNICName))
|
||||
qdisc1 := &netlink.Ingress{
|
||||
QdiscAttrs: netlink.QdiscAttrs{
|
||||
LinkIndex: fromNICID,
|
||||
Parent: netlink.HANDLE_INGRESS,
|
||||
},
|
||||
}
|
||||
|
||||
if err := netlink.QdiscAdd(qdisc1); err != nil {
|
||||
return fmt.Errorf("failed to add qdisc for interface %s: %v", fromNICName, err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("step 2: tc filter add dev %s parent ffff: protocol ip u32 match u8 0 0 action mirred egress mirror dev %s", fromNICName, toNICName))
|
||||
// add a filter to mirror traffic from index1 to index2
|
||||
filter1 := &netlink.U32{
|
||||
FilterAttrs: netlink.FilterAttrs{
|
||||
LinkIndex: fromNICID,
|
||||
Parent: netlink.MakeHandle(0xffff, 0),
|
||||
Protocol: unix.ETH_P_ALL,
|
||||
},
|
||||
Actions: []netlink.Action{
|
||||
&netlink.MirredAction{
|
||||
ActionAttrs: netlink.ActionAttrs{
|
||||
Action: netlink.TC_ACT_PIPE,
|
||||
},
|
||||
MirredAction: netlink.TCA_EGRESS_MIRROR,
|
||||
Ifindex: toNICID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := netlink.FilterAdd(filter1); err != nil {
|
||||
return fmt.Errorf("failed to add filter for interface %s: %v", fromNICName, err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("step 3: tc qdisc add dev %s ingress", fromNICName))
|
||||
qdiscTemp := netlink.NewPrio(netlink.QdiscAttrs{
|
||||
LinkIndex: fromNICID,
|
||||
Parent: netlink.HANDLE_ROOT,
|
||||
})
|
||||
|
||||
if err := netlink.QdiscReplace(qdiscTemp); err != nil {
|
||||
return fmt.Errorf("failed to replace qdisc with prio type qdisc: %v", err)
|
||||
}
|
||||
|
||||
// get id through tc qdisc show dev fromNICName
|
||||
qdiscID, err := getQdiscFromInterfaceByType(fromNICID, fromNICName, "ingress")
|
||||
if err != nil {
|
||||
if err == errQdiscNotFound {
|
||||
return fmt.Errorf("no qdisc under interface %s is prio type: %v", fromNICName, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("step 4: tc filter add dev %s parent %d: protocol ip u32 match u8 0 0 action mirred egress mirror dev %s", fromNICName, qdiscID, toNICName))
|
||||
|
||||
filter2 := &netlink.U32{
|
||||
FilterAttrs: netlink.FilterAttrs{
|
||||
LinkIndex: fromNICID,
|
||||
Parent: netlink.MakeHandle(uint16(qdiscID), 0), //nolint
|
||||
Protocol: unix.ETH_P_ALL,
|
||||
},
|
||||
Actions: []netlink.Action{
|
||||
&netlink.MirredAction{
|
||||
ActionAttrs: netlink.ActionAttrs{
|
||||
Action: netlink.TC_ACT_PIPE,
|
||||
},
|
||||
MirredAction: netlink.TCA_EGRESS_MIRROR,
|
||||
Ifindex: toNICID,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := netlink.FilterAdd(filter2); err != nil {
|
||||
return fmt.Errorf("failed to add filter for interface %s: %v", fromNICName, err)
|
||||
}
|
||||
|
||||
log.Info("traffic mirroring has been set up", "src", toNICName, "dst", fromNICName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupQDSICFromNIC cleans up all qdisc config on interface
|
||||
func CleanupQDSICFromNIC(nicName string) error {
|
||||
// name of nic which traffic will be mirrored to
|
||||
toNIC, err := netlink.LinkByName(nicName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find nic %s: %v", nicName, err)
|
||||
}
|
||||
nicID := toNIC.Attrs().Index
|
||||
|
||||
log.Debug(fmt.Sprintf("interface %s has index %d", nicName, nicID))
|
||||
|
||||
log.Debug("step 1: delete ingress qdisc")
|
||||
if err := tryCleanupQdiscByType(nicID, nicName, "ingress"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("step 2: delete root prio qdisc")
|
||||
if err := tryCleanupQdiscByType(nicID, nicName, "prio"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("finished cleaning up all qdisc config", "interface", nicName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getQdiscFromInterfaceByType(nicID int, nicName string, qType string) (uint32, error) {
|
||||
// get id through tc qdisc show dev fromNICName
|
||||
qs, err := netlink.QdiscList(&netlink.Ifb{LinkAttrs: netlink.LinkAttrs{Index: nicID}})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to list qdisc for interface %s: %v", nicName, err)
|
||||
return 0, err
|
||||
}
|
||||
for _, q := range qs {
|
||||
if q.Type() == qType {
|
||||
return q.Attrs().Handle, nil
|
||||
}
|
||||
}
|
||||
log.Error("no qdisc", "interface", nicName, "is of type", qType)
|
||||
return 0, errQdiscNotFound
|
||||
}
|
||||
|
||||
func tryCleanupQdiscByType(nicID int, nicName, qType string) error {
|
||||
var qdisc netlink.Qdisc
|
||||
switch qType {
|
||||
case "ingress":
|
||||
qdisc = &netlink.Ingress{QdiscAttrs: netlink.QdiscAttrs{
|
||||
LinkIndex: nicID,
|
||||
Parent: netlink.HANDLE_INGRESS,
|
||||
}}
|
||||
case "prio":
|
||||
qdisc = netlink.NewPrio(netlink.QdiscAttrs{
|
||||
LinkIndex: nicID,
|
||||
Parent: netlink.HANDLE_ROOT,
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("unknown qdisc type %s", qType)
|
||||
}
|
||||
|
||||
_, err := getQdiscFromInterfaceByType(nicID, nicName, qType)
|
||||
if err != nil {
|
||||
if err == errQdiscNotFound {
|
||||
log.Debug(fmt.Sprintf("%s type qdisc doesn't exist on interface %s, skip deleting", qType, nicName))
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := netlink.QdiscDel(qdisc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
128
pkg/upnp/upnp.go
Normal file
128
pkg/upnp/upnp.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/huin/goupnp"
|
||||
"github.com/huin/goupnp/dcps/internetgateway2"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// The UPNP library is structured a bit funny. To create a forward on a gateway and know
|
||||
// the external IP of that gateway we need two different instances of the UPNP client
|
||||
// WANIPv6FirewallControlClient can be empty if the gatway doesn't support the WANIPv6FirewallControl service
|
||||
type Gateway struct {
|
||||
ConnectionClient ConnectionClient
|
||||
WANIPv6FirewallControlClient *internetgateway2.WANIPv6FirewallControl1
|
||||
}
|
||||
|
||||
type ConnectionClient interface {
|
||||
AddPortMapping(
|
||||
NewRemoteHost string,
|
||||
NewExternalPort uint16,
|
||||
NewProtocol string,
|
||||
NewInternalPort uint16,
|
||||
NewInternalClient string,
|
||||
NewEnabled bool,
|
||||
NewPortMappingDescription string,
|
||||
NewLeaseDuration uint32,
|
||||
) (err error)
|
||||
|
||||
GetExternalIPAddress() (
|
||||
NewExternalIPAddress string,
|
||||
err error,
|
||||
)
|
||||
GetServiceClient() *goupnp.ServiceClient
|
||||
}
|
||||
|
||||
func GetGatewayClients(ctx context.Context) []Gateway {
|
||||
clients := GetConnectionClients(ctx)
|
||||
gatewayClients := make([]Gateway, len(clients))
|
||||
|
||||
for i := range clients {
|
||||
gatewayClients[i] = Gateway{ConnectionClient: clients[i], WANIPv6FirewallControlClient: nil}
|
||||
gatewayURL := clients[i].GetServiceClient().Location
|
||||
if wanipv6clients, err := internetgateway2.NewWANIPv6FirewallControl1ClientsByURLCtx(ctx, gatewayURL); err == nil {
|
||||
gatewayClients[i].WANIPv6FirewallControlClient = wanipv6clients[0]
|
||||
} else {
|
||||
log.Warn("[UPNP] Unable to find WANIPv6FirewallControl1Clients", "Gateway", gatewayURL, "err", err)
|
||||
}
|
||||
}
|
||||
return gatewayClients
|
||||
}
|
||||
|
||||
// Gather UPNPConnectionClients in the network and treats errors as non-critical. Use this to find out the external IPs of the network and configure port forwarding
|
||||
func GetConnectionClients(ctx context.Context) []ConnectionClient {
|
||||
tasks, _ := errgroup.WithContext(ctx)
|
||||
// This code sucks and I did not manage to make it more concise.
|
||||
//Turns out using []*type instead of []type prevents you from casting to an interface.
|
||||
// If you have any better ideas please take a stab at it.
|
||||
var ip1Clients []*internetgateway2.WANIPConnection1
|
||||
var ip1Error []error
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ip1Clients, ip1Error, err = internetgateway2.NewWANIPConnection1Clients()
|
||||
return err
|
||||
})
|
||||
var ip2Clients []*internetgateway2.WANIPConnection2
|
||||
var ip2Error []error
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ip2Clients, ip2Error, err = internetgateway2.NewWANIPConnection2Clients()
|
||||
return err
|
||||
})
|
||||
var ppp1Clients []*internetgateway2.WANPPPConnection1
|
||||
var ppp1Error []error
|
||||
tasks.Go(func() error {
|
||||
var err error
|
||||
ppp1Clients, ppp1Error, err = internetgateway2.NewWANPPPConnection1Clients()
|
||||
return err
|
||||
})
|
||||
|
||||
var routers []ConnectionClient
|
||||
|
||||
if err := tasks.Wait(); err != nil {
|
||||
log.Error("[UPNP] Could not finish querying UPNP connection clients", "err", err.Error())
|
||||
return routers
|
||||
}
|
||||
|
||||
errors := append(ip1Error, ip2Error...)
|
||||
errors = append(errors, ppp1Error...)
|
||||
|
||||
for _, e := range errors {
|
||||
log.Warn("[UPNP] UPNP Gateway responded with an error while querying WAN Connection Client", "err", e)
|
||||
}
|
||||
|
||||
for _, c := range ip1Clients {
|
||||
if c != nil {
|
||||
routers = append(routers, c)
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range ip2Clients {
|
||||
if c != nil {
|
||||
routers = append(routers, c)
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range ppp1Clients {
|
||||
if c != nil {
|
||||
routers = append(routers, c)
|
||||
}
|
||||
}
|
||||
return routers
|
||||
}
|
||||
|
||||
func MapProtocolToIANA(p string) uint16 {
|
||||
switch strings.ToUpper(p) {
|
||||
case "TCP":
|
||||
return 6
|
||||
case "UDP":
|
||||
return 17
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
72
pkg/utils/dns.go
Normal file
72
pkg/utils/dns.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
IPv4Family = "IPv4"
|
||||
IPv6Family = "IPv6"
|
||||
DualFamily = "dual"
|
||||
)
|
||||
|
||||
// LookupHost resolves dnsName and return an IP or an error
|
||||
func LookupHost(dnsName, dnsMode string) ([]string, error) {
|
||||
result, err := net.LookupHost(dnsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, errors.Errorf("empty address for %s", dnsName)
|
||||
}
|
||||
addrs := []string{}
|
||||
switch dnsMode {
|
||||
case strings.ToLower(IPv4Family), strings.ToLower(IPv6Family), DualFamily:
|
||||
a, err := getIPbyFamily(result, dnsMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs = append(addrs, a...)
|
||||
default:
|
||||
addrs = append(addrs, result[0])
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func getIPbyFamily(addresses []string, family string) ([]string, error) {
|
||||
var checkers []func(string) bool
|
||||
families := []string{}
|
||||
if family == DualFamily || family == strings.ToLower(IPv4Family) {
|
||||
checkers = append(checkers, IsIPv4)
|
||||
families = append(families, IPv4Family)
|
||||
}
|
||||
if family == DualFamily || family == strings.ToLower(IPv6Family) {
|
||||
checkers = append(checkers, IsIPv6)
|
||||
families = append(families, IPv6Family)
|
||||
}
|
||||
|
||||
addrs := []string{}
|
||||
for i, c := range checkers {
|
||||
addr, err := getIPbyChecker(addresses, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting %s address: %w", families[i], err)
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func getIPbyChecker(addresses []string, checker func(string) bool) (string, error) {
|
||||
for _, addr := range addresses {
|
||||
if checker(addr) {
|
||||
return addr, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("address not found")
|
||||
}
|
||||
58
pkg/utils/ip.go
Normal file
58
pkg/utils/ip.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// FormatIPWithSubnetMask takes a raw IP address and a subnet mask, and returns a formatted string in CIDR notation.
|
||||
func FormatIPWithSubnetMask(rawIP string, subnetMask string) (string, error) {
|
||||
|
||||
addr := fmt.Sprintf("%s/%s", rawIP, subnetMask)
|
||||
// Check if the input is valid
|
||||
_, _, err := net.ParseCIDR(addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid CIDR: %q, %w", addr, err)
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// IsIP returns if address is an IP or not
|
||||
func IsIP(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
// IsIPv4 returns true only if address is a valid IPv4 address
|
||||
func IsIPv4(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() != nil
|
||||
}
|
||||
|
||||
// IsIPv6 returns true only if address is a valid IPv6 address
|
||||
func IsIPv6(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
|
||||
func IsIPv4CIDR(cidr string) bool {
|
||||
ip, _, _ := net.ParseCIDR(cidr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() != nil
|
||||
}
|
||||
|
||||
func IsIPv6CIDR(cidr string) bool {
|
||||
ip, _, _ := net.ParseCIDR(cidr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user