Compare commits

...

467 Commits

Author SHA1 Message Date
Dan Finneran
296c9c1580 Unintentionally created duplicate functions that can't be used
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-08-12 12:17:20 +00:00
Daniel Finneran
d8877072d4 Merge pull request #1219 from kube-vip/v1-release
Version ONE DOT ZERO
2025-07-31 16:42:23 +01:00
Dan Finneran
89a8dc7de1 default setting to 0
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-31 16:09:10 +02:00
Dan Finneran
704c346f5e naming convention lint fix
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-31 16:09:10 +02:00
Dan Finneran
65061c5cd9 Adds an annotation to set the return path filter for a macvlan nic
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-31 16:09:10 +02:00
Daniel Finneran
32233918b4 Version ONE DOT ZERO 2025-07-31 12:34:37 +01:00
Daniel Finneran
76169da60f Merge pull request #1216 from kube-vip/dependabot/go_modules/github.com/docker/docker-28.3.3incompatible
Bump github.com/docker/docker from 28.3.0+incompatible to 28.3.3+incompatible
2025-07-31 12:33:44 +01:00
Daniel Finneran
9bcf1413f0 Merge pull request #1211 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.20.4
Bump anchore/sbom-action from 0.20.1 to 0.20.4
2025-07-31 12:33:28 +01:00
Daniel Finneran
8e428e875f Merge pull request #1203 from kube-vip/dependabot/docker/alpine-3.22.1
Bump alpine from 3.22.0 to 3.22.1
2025-07-31 12:33:14 +01:00
dependabot[bot]
2fbecc25e5 Bump alpine from 3.22.0 to 3.22.1
Bumps alpine from 3.22.0 to 3.22.1.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: 3.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 15:17:21 +00:00
Daniel Finneran
02e77271d0 Merge pull request #1202 from kube-vip/dependabot/docker/golang-1.24.5-alpine3.22 2025-07-30 16:16:18 +01:00
Daniel Finneran
3d61888e58 Merge pull request #1192 from kube-vip/dependabot/go_modules/sigs.k8s.io/kind-0.29.0 2025-07-30 16:15:52 +01:00
dependabot[bot]
efe75f491b Bump sigs.k8s.io/kind from 0.27.0 to 0.29.0
Bumps [sigs.k8s.io/kind](https://github.com/kubernetes-sigs/kind) from 0.27.0 to 0.29.0.
- [Release notes](https://github.com/kubernetes-sigs/kind/releases)
- [Commits](https://github.com/kubernetes-sigs/kind/compare/v0.27.0...v0.29.0)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/kind
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 13:55:46 +00:00
Daniel Finneran
000c139004 Merge pull request #1194 from kube-vip/dependabot/go_modules/github.com/vishvananda/netlink-1.3.1 2025-07-30 14:50:38 +01:00
Daniel Finneran
c39b84f0a9 Merge pull request #1193 from kube-vip/dependabot/go_modules/github.com/osrg/gobgp/v3-3.37.0 2025-07-30 14:50:25 +01:00
dependabot[bot]
ee958addaa Bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.3.0+incompatible to 28.3.3+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.3.0...v28.3.3)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.3.3+incompatible
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 13:48:44 +00:00
Daniel Finneran
8fe53351f8 Merge pull request #1215 from kube-vip/endpointslice 2025-07-30 14:47:26 +01:00
Daniel Finneran
bc9d860d83 Merge pull request #1213 from kube-vip/egress_internal 2025-07-30 14:47:07 +01:00
Dan Finneran
332a23e543 Bump k8s api and endpointslices as default
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-29 13:36:32 +00:00
Dan Finneran
b20713b50f linting issue with port out of bounds.
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-28 16:18:47 +00:00
Dan Finneran
94e96581ef typo and add SCTP
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-28 16:13:20 +00:00
Dan Finneran
61be6d0b6a fighting with merge conflicts
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-28 16:08:53 +00:00
Dan Finneran
be22805a7d sigh
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-28 16:08:22 +00:00
Dan Finneran
25f6253286 Adds the internal egress functionality
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-28 16:01:00 +00:00
dependabot[bot]
f3e9fb6ea9 Bump github.com/osrg/gobgp/v3 from 3.36.0 to 3.37.0
Bumps [github.com/osrg/gobgp/v3](https://github.com/osrg/gobgp) from 3.36.0 to 3.37.0.
- [Release notes](https://github.com/osrg/gobgp/releases)
- [Changelog](https://github.com/osrg/gobgp/blob/master/.goreleaser.yml)
- [Commits](https://github.com/osrg/gobgp/compare/v3.36.0...v3.37.0)

---
updated-dependencies:
- dependency-name: github.com/osrg/gobgp/v3
  dependency-version: 3.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 15:17:29 +00:00
Patryk Strusiewicz-Surmacki
0f3dda02c4 Refactoring services-watcher code
Co-authored-by: Cellebyte <marcel.fest@live.de>
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-28 17:14:09 +02:00
Dan Finneran
a2873b5465 Adds the internal egress functionality
Signed-off-by: Dan Finneran <dan@thebsdbox.co.uk>
2025-07-28 13:57:50 +00:00
dependabot[bot]
202d45e5ab Bump anchore/sbom-action from 0.20.1 to 0.20.4
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.20.1 to 0.20.4.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.20.1...v0.20.4)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 03:11:16 +00:00
Daniel Finneran
f5e4612c03 Merge pull request #1210 from kube-vip/fix/ci 2025-07-27 22:14:39 +01:00
Cellebyte
de888c501c split ci into two files
Signed-off-by: Cellebyte <marcel.fest@live.de>
2025-07-27 23:02:09 +02:00
Cellebyte
47bc83c248 Don't execute the e2e-tests in parallel
Signed-off-by: Cellebyte <marcel.fest@live.de>
2025-07-27 23:02:09 +02:00
Patryk Strusiewicz-Surmacki
1cf637c569 Moved endpoint-related code from pkg/manager to pkg/endpoints
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-24 08:07:23 +02:00
Patryk Strusiewicz-Surmacki
ae2571e241 Refactoring endpoint-watcher code
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-24 08:07:23 +02:00
Patryk Strusiewicz-Surmacki
86f5e9b8b2 Removed insecure client use in E2E tests
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-24 08:07:00 +02:00
Patryk Strusiewicz-Surmacki
ce61ff085a Use insecure client only in E2E tests
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-21 17:54:12 +02:00
dependabot[bot]
b816e154cf Bump golang from 1.24.4-alpine3.22 to 1.24.5-alpine3.22
Bumps golang from 1.24.4-alpine3.22 to 1.24.5-alpine3.22.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.5-alpine3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 03:15:14 +00:00
Patryk Strusiewicz-Surmacki
ac1238c337 Rebase
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-10 17:50:15 +02:00
Patryk Strusiewicz-Surmacki
10dbf2c0ef Refactoring endpoint-watcher code
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-10 17:50:15 +02:00
Jan-Philipp Litza
cf68f8639c Add VIPs as deprecated
This causes IPv6 source address selection according to RFC 3484 to
ignore VIPs when choosing which address the host should use for egress
communication.

Signed-off-by: Jan-Philipp Litza <janphilipp@litza.de>
2025-07-09 10:54:28 +02:00
Daniel Finneran
b114c11b0f Merge pull request #1191 from kube-vip/thebsdbox-patch-1
Update README.md
2025-07-03 09:59:49 +01:00
Patryk Strusiewicz-Surmacki
42b7a8152b Basic e2e tests for BGP mode
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-07-01 12:36:33 +02:00
mushrushu
f7821c7fb3 fixed vars formatting & log statement
Signed-off-by: mushrushu <mushrushu@outlook.com>
2025-07-01 12:34:43 +02:00
mushrushu
42478905d0 added zebra integration and its configuration
Signed-off-by: mushrushu <mushrushu@outlook.com>
2025-07-01 12:34:43 +02:00
dependabot[bot]
8d55bd3b63 Bump github.com/vishvananda/netlink from 1.3.0 to 1.3.1
Bumps [github.com/vishvananda/netlink](https://github.com/vishvananda/netlink) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/vishvananda/netlink/releases)
- [Commits](https://github.com/vishvananda/netlink/compare/v1.3.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/vishvananda/netlink
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 03:20:14 +00:00
Daniel Finneran
16247fc3a3 Update README.md
Adds health of project badges! (looking healthy) 😄
2025-06-27 14:20:14 +01:00
Marcel Fest
b56b80cd30 Bump version to v0.9.2
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-24 19:02:31 +02:00
dependabot[bot]
ba25e0e583 Bump github.com/prometheus/client_golang from 1.21.0 to 1.22.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.0...v1.22.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 18:59:16 +02:00
dependabot[bot]
8f1fe355fc Bump github.com/onsi/gomega from 1.36.2 to 1.37.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.36.2 to 1.37.0.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.36.2...v1.37.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-24 16:29:41 +02:00
dependabot[bot]
649d9bf0ef Bump golang.org/x/sys from 0.32.0 to 0.33.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.32.0 to 0.33.0.
- [Commits](https://github.com/golang/sys/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 09:20:35 +02:00
dependabot[bot]
a5108a69aa Bump anchore/sbom-action from 0.19.0 to 0.20.1
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.19.0 to 0.20.1.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.19.0...v0.20.1)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-20 17:53:00 +02:00
dependabot[bot]
c74a496299 Bump go.etcd.io/etcd/client/v3 from 3.5.18 to 3.6.1
Bumps [go.etcd.io/etcd/client/v3](https://github.com/etcd-io/etcd) from 3.5.18 to 3.6.1.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.18...v3.6.1)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/client/v3
  dependency-version: 3.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-17 12:06:43 +02:00
dependabot[bot]
b74c274466 Bump alpine from 3.21.3 to 3.22.0
Bumps alpine from 3.21.3 to 3.22.0.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: 3.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-17 11:57:04 +02:00
Dan Finneran
56b3867e57 typo fix 2025-06-17 11:56:43 +02:00
Dan Finneran
66d237bfbc adds kernel module check for nftables 2025-06-17 11:56:43 +02:00
Patryk Strusiewicz-Surmacki
68071b214e Fixed BGP clear deadlock
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-06-17 11:55:49 +02:00
Patryk Strusiewicz-Surmacki
a82ca5576b Updated go version to 1.24.4
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-06-17 09:53:57 +02:00
Patryk Strusiewicz-Surmacki
81dd386b4e Fixed service-tests
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
Co-authored-by: Marcel Fest <marcel@fest4.eu>
2025-06-10 22:26:02 +02:00
Patryk Strusiewicz-Surmacki
b61a74396d Moved configuredRoutes map to serviceContext struct
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-06-10 22:26:02 +02:00
Daniel Finneran
105fbc522a Merge pull request #1163 from p-strusiewiczsurmacki-mobica/rt-tests
Added ARP/NDP services tests and Routing Table services tests
2025-06-10 11:34:15 -07:00
Dan Finneran
2b52c39242 Small change to reduce pointless errors 2025-06-10 13:31:06 +02:00
Patryk Strusiewicz-Surmacki
25d39bca09 Added TODO for insecure discovery client
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-06-06 16:18:38 +02:00
Marcel Fest
644226321e fix typo
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
3928dda541 move lock for cleanRoutes feature at start of kube-vip
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Patryk Strusiewicz-Surmacki
d9a7f413a6 Moved code to use sync.Maps
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Patryk Strusiewicz-Surmacki
eb300bb634 Fixed deadlock
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
c30fd9e7be fix tests + refactor func
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
2fc969b848 Move the check into the function
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
47884088ec remove AI code
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
70e1212396 remove redundant condition
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
d947c2abcc refactor syncServices to be lockable at all
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
e2efb64aea Use the mutext in callbacks as well
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Marcel Fest
01279d45e3 Added function to create ip array from statu LB ingress field
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-06-05 17:26:55 +02:00
Patryk Strusiewicz-Surmacki
f2a7cad218 Added ARP/NDP services tests and Routing Table services tests
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-05-23 11:59:14 +02:00
Nathan Przybyszewski
98163341d3 Replace invalid verb "endpoints" with "update"
Signed-off-by: Nathan Przybyszewski <172687074+nprzy@users.noreply.github.com>
2025-05-01 10:28:43 +02:00
Marcel Fest
a71d361d15 Bump version
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-28 16:33:49 +02:00
dependabot[bot]
cf24ad835d Bump github.com/onsi/ginkgo/v2 from 2.22.2 to 2.23.4
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.22.2 to 2.23.4.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.22.2...v2.23.4)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-version: 2.23.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 14:21:36 +02:00
dependabot[bot]
0c04088b16 Bump github.com/cloudflare/ipvs from 0.10.3 to 0.11.0
Bumps [github.com/cloudflare/ipvs](https://github.com/cloudflare/ipvs) from 0.10.3 to 0.11.0.
- [Release notes](https://github.com/cloudflare/ipvs/releases)
- [Commits](https://github.com/cloudflare/ipvs/compare/v0.10.3...v0.11.0)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/ipvs
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 14:07:42 +02:00
dependabot[bot]
22489ad095 Bump golang.org/x/sys from 0.31.0 to 0.32.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.31.0 to 0.32.0.
- [Commits](https://github.com/golang/sys/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 14:07:29 +02:00
dependabot[bot]
1fa3da45fa Bump github.com/gookit/slog from 0.5.7 to 0.5.8
Bumps [github.com/gookit/slog](https://github.com/gookit/slog) from 0.5.7 to 0.5.8.
- [Release notes](https://github.com/gookit/slog/releases)
- [Commits](https://github.com/gookit/slog/compare/v0.5.7...v0.5.8)

---
updated-dependencies:
- dependency-name: github.com/gookit/slog
  dependency-version: 0.5.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 14:07:18 +02:00
Patryk Strusiewicz-Surmacki
d497df3767 Fixed instance finding and route deletion in RT mode
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-28 14:06:20 +02:00
dependabot[bot]
bdd3c5c191 Bump anchore/sbom-action from 0.18.0 to 0.19.0
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 07:17:13 +02:00
Shawn Wilsher
5b41db2246 feat: Add opencontainers annotations during release
This change relies on `docker/metadata-action` to generate the
appropriate labels.

Fixes #1044

Signed-off-by: Shawn Wilsher <656602+sdwilsh@users.noreply.github.com>
2025-04-27 21:52:29 +02:00
Shawn Wilsher
1eb35774a5 fix: s/endoints/endpoints/ in RBAC manifest generation code
While moving over to using the RBAC manifest generator, this stood out as a clear typo.

Signed-off-by: Shawn Wilsher <me@shawnwilsher.com>
2025-04-27 09:27:12 +02:00
Daniel Finneran
7d7036fae9 Merge pull request #1130 from kube-vip/fix/upnp
fix #1127 as we have a nil deref if router does not support IPv6
2025-04-15 17:15:23 +01:00
Marcel Fest
1a4bf13819 fix #1127 as we have a nil deref if router does not support IPv6
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 23:20:29 +02:00
dependabot[bot]
3e225bf51f Bump go.etcd.io/etcd/api/v3 from 3.5.18 to 3.5.21
Bumps [go.etcd.io/etcd/api/v3](https://github.com/etcd-io/etcd) from 3.5.18 to 3.5.21.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.18...v3.5.21)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/api/v3
  dependency-version: 3.5.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 23:16:09 +02:00
dependabot[bot]
d6837cbe7d Bump github.com/osrg/gobgp/v3 from 3.35.0 to 3.36.0
Bumps [github.com/osrg/gobgp/v3](https://github.com/osrg/gobgp) from 3.35.0 to 3.36.0.
- [Release notes](https://github.com/osrg/gobgp/releases)
- [Changelog](https://github.com/osrg/gobgp/blob/master/.goreleaser.yml)
- [Commits](https://github.com/osrg/gobgp/compare/v3.35.0...v3.36.0)

---
updated-dependencies:
- dependency-name: github.com/osrg/gobgp/v3
  dependency-version: 3.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 22:56:35 +02:00
dependabot[bot]
40994e0464 Bump go.etcd.io/etcd/client/pkg/v3 from 3.5.18 to 3.5.21
Bumps [go.etcd.io/etcd/client/pkg/v3](https://github.com/etcd-io/etcd) from 3.5.18 to 3.5.21.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.18...v3.5.21)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/client/pkg/v3
  dependency-version: 3.5.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 22:54:20 +02:00
dependabot[bot]
4e18ad189d Bump google.golang.org/protobuf from 1.36.5 to 1.36.6
Bumps google.golang.org/protobuf from 1.36.5 to 1.36.6.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 22:39:06 +02:00
dependabot[bot]
a0ed07913e Bump golang.org/x/sync from 0.11.0 to 0.13.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.11.0 to 0.13.0.
- [Commits](https://github.com/golang/sync/compare/v0.11.0...v0.13.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 22:20:57 +02:00
Patryk Strusiewicz-Surmacki
3d9ca62031 Removed dependancy for endpoint counter in ARP mode
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Patryk Strusiewicz-Surmacki
e0f4520437 Fixed ARP multiple references issues
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk.pawel.strusiewicz-surmacki@external.telekom.de>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
3a5ebd184d Setup ipv4 and ipv6 subnet mask in template
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
a7d19c15f1 rename forgotten envvar
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
9822d92bed remove wrong if condition
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
4d8b7750ae fix error wrap
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Patryk Strusiewicz-Surmacki
3bcf783020 Fixed E2E tests
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
1b3a7bb5de improved log messages
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
0d5ac98209 Simplify validation check and remove unused code
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
e2472e509b fix tests?
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
08388496bc Updated logic to not be as redundant
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
f4eab023e8 Refactor and condense code, we always want to set SubnetMask
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
c5e854f323 Breaking: Remove vip_cidr in favor of vip_subnet from config interface
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
e7b9439161 Breaking Change do to retirement of vip_cidr
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
555ca2b830 fix ci
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Marcel Fest
271f21f203 Try to add dual-stack support for BGP mode
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-14 21:41:34 +02:00
Daniel Finneran
de54fcbd11 Merge pull request #1126 from kube-vip/add/rbac-manifest-generation
Added support for RBAC manifest generation
2025-04-10 15:50:40 +01:00
Patryk Strusiewicz-Surmacki
46cad395e1 Fixed service IP address deletion on service modification bug
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-09 23:15:43 +02:00
Marcel Fest
3466947f69 Added doc string for RBAC manifest generation
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-09 23:06:22 +02:00
Marcel Fest
8696f80525 added missing inCluster flag back
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-09 23:06:22 +02:00
Marcel Fest
51527d3e6c Added support for RBAC manifest generation
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-09 23:06:20 +02:00
Marcel Fest
c8a9189bf6 Remove the packet env var constants
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-09 21:11:39 +02:00
Deveshi Dwivedi
896b0983e4 remove remaining providerConfig references
Signed-off-by: Deveshi Dwivedi <deveshigurgaon@gmail.com>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-09 21:11:39 +02:00
Deveshi Dwivedi
d55b124251 change annotations format back
Signed-off-by: Deveshi Dwivedi <deveshigurgaon@gmail.com>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-09 21:11:39 +02:00
Deveshi Dwivedi
72b6e22d90 remove equinix-metal code
Signed-off-by: Deveshi Dwivedi <deveshigurgaon@gmail.com>
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2025-04-09 21:11:39 +02:00
Marcel Fest
3272bc1f8b Update golang-ci-lint to use kubernetes 1.24.x 2025-04-04 16:54:39 +02:00
dependabot[bot]
1e754703e8 Bump k8s.io/client-go from 0.32.1 to 0.32.3
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.32.1 to 0.32.3.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.32.1...v0.32.3)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-04 16:54:39 +02:00
dependabot[bot]
119424bb9b Bump k8s.io/api from 0.32.1 to 0.32.3
Bumps [k8s.io/api](https://github.com/kubernetes/api) from 0.32.1 to 0.32.3.
- [Commits](https://github.com/kubernetes/api/compare/v0.32.1...v0.32.3)

---
updated-dependencies:
- dependency-name: k8s.io/api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-04 16:54:39 +02:00
Marcel Fest
d8926ebea5 bump golang 2025-04-04 16:54:39 +02:00
dependabot[bot]
6b1a0a7ea8 Bump github.com/osrg/gobgp/v3 from 3.34.0 to 3.35.0
Bumps [github.com/osrg/gobgp/v3](https://github.com/osrg/gobgp) from 3.34.0 to 3.35.0.
- [Release notes](https://github.com/osrg/gobgp/releases)
- [Changelog](https://github.com/osrg/gobgp/blob/master/.goreleaser.yml)
- [Commits](https://github.com/osrg/gobgp/compare/v3.34.0...v3.35.0)

---
updated-dependencies:
- dependency-name: github.com/osrg/gobgp/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 16:52:27 +02:00
dependabot[bot]
c92d01b957 Bump golang.org/x/net from 0.34.0 to 0.36.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.34.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.34.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 16:51:26 +02:00
dependabot[bot]
d6c6d8e529 Bump sigs.k8s.io/kind from 0.26.0 to 0.27.0
Bumps [sigs.k8s.io/kind](https://github.com/kubernetes-sigs/kind) from 0.26.0 to 0.27.0.
- [Release notes](https://github.com/kubernetes-sigs/kind/releases)
- [Commits](https://github.com/kubernetes-sigs/kind/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/kind
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 16:50:41 +02:00
dependabot[bot]
1d086b2d5c Bump golang from 1.24.0-alpine3.20 to 1.24.1-alpine3.20
Bumps golang from 1.24.0-alpine3.20 to 1.24.1-alpine3.20.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 16:49:48 +02:00
dependabot[bot]
e5d967dcf4 Bump github.com/spf13/cobra from 1.8.1 to 1.9.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.1 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-03 16:49:15 +02:00
Patryk Strusiewicz-Surmacki
21a5533936 Fixes for IPv6 and dualstack clusters
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:19:06 +02:00
Patryk Strusiewicz-Surmacki
d21fec6c7d Cancel leaderelection if forwarding mode is local and no local endpoints are present
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:19:06 +02:00
Patryk Strusiewicz-Surmacki
9c4ae86937 Added IPv6 related configuration for IPVS
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:19:06 +02:00
Patryk Strusiewicz-Surmacki
9c67660b94 Added permission check for IPVS info
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:19:06 +02:00
Patryk Strusiewicz-Surmacki
96d020b6ad Disable IPVS related iptables rules if LB is not enabled
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:19:06 +02:00
Patryk Strusiewicz-Surmacki
bc42c3e2c3 Added sysctl check
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:19:06 +02:00
Patryk Strusiewicz-Surmacki
ea45dafcf3 Fixed IPVS deletion issues
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:19:06 +02:00
Patryk Strusiewicz-Surmacki
3e3fd21c16 Fixed route deletion issue when 2 services referenced same endpoint
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-04-03 16:04:11 +02:00
Patryk Strusiewicz-Surmacki
70b83664a9 Less strict service status update error handling
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-03-27 15:27:26 +01:00
Patryk Strusiewicz-Surmacki
9650a00157 Fixed service/endpoints discovery concurrent issue
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-03-10 12:12:23 +01:00
Patryk Strusiewicz-Surmacki
f851ddbcaf Fixed service update in BGP and ARP mode
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-03-08 08:43:00 +01:00
Daniel Finneran
a5ee4d969b Merge pull request #1091 from p-strusiewiczsurmacki-mobica/ipvs-re-add-cp
Fixed IPVS backend re-add
2025-03-06 17:15:24 +01:00
Patryk Strusiewicz-Surmacki
2dfd46decb Fixed IPVS backend re-add
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-03-04 15:42:45 +01:00
Daniel Finneran
6752dc0fdd Merge pull request #1084 from kube-vip/dependabot/go_modules/github.com/prometheus/client_golang-1.21.0
Bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0
2025-02-26 15:45:38 +01:00
Daniel Finneran
c7174d3c94 Merge pull request #1083 from kube-vip/dependabot/go_modules/github.com/google/go-cmp-0.7.0
Bump github.com/google/go-cmp from 0.6.0 to 0.7.0
2025-02-26 15:45:25 +01:00
Daniel Finneran
d33ba7e22e Merge pull request #1079 from kube-vip/health
This adds a health check
2025-02-26 15:44:29 +01:00
dependabot[bot]
9b552a4d29 Bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.5 to 1.21.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.5...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 02:57:33 +00:00
dependabot[bot]
5fc04a24f8 Bump github.com/google/go-cmp from 0.6.0 to 0.7.0
Bumps [github.com/google/go-cmp](https://github.com/google/go-cmp) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/google/go-cmp/releases)
- [Commits](https://github.com/google/go-cmp/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: github.com/google/go-cmp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 02:57:30 +00:00
Daniel Finneran
122b18a81d Merge pull request #1070 from kube-vip/dependabot/go_modules/google.golang.org/protobuf-1.36.5
Bump google.golang.org/protobuf from 1.36.1 to 1.36.5
2025-02-20 12:42:39 +01:00
Daniel Finneran
c9bc005abe Merge pull request #1075 from kube-vip/dependabot/go_modules/github.com/cloudflare/ipvs-0.10.3
Bump github.com/cloudflare/ipvs from 0.10.2 to 0.10.3
2025-02-20 12:42:26 +01:00
Daniel Finneran
ee0f0668ab Merge pull request #1076 from kube-vip/dependabot/docker/alpine-3.21.3
Bump alpine from 3.21.2 to 3.21.3
2025-02-20 12:42:09 +01:00
Daniel Finneran
826bb6fc8a Merge pull request #1077 from kube-vip/dependabot/docker/golang-1.24.0-alpine3.20
Bump golang from 1.23.5-alpine3.20 to 1.24.0-alpine3.20
2025-02-20 12:41:57 +01:00
Dan Finneran
b9aa99a208 more lint goodness 2025-02-17 17:23:01 +00:00
Dan Finneran
2b0aa825b5 lint fix 2025-02-17 17:21:20 +00:00
dependabot[bot]
24b1524aee Bump google.golang.org/protobuf from 1.36.1 to 1.36.5
Bumps google.golang.org/protobuf from 1.36.1 to 1.36.5.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 17:18:22 +00:00
Daniel Finneran
cf7c7f2f25 Merge pull request #1069 from kube-vip/dependabot/go_modules/golang.org/x/sync-0.11.0
Bump golang.org/x/sync from 0.10.0 to 0.11.0
2025-02-17 18:17:01 +01:00
Dan Finneran
c082688e67 This adds a health check 2025-02-17 17:16:29 +00:00
dependabot[bot]
18bfe6a8d2 Bump golang.org/x/sync from 0.10.0 to 0.11.0
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/sync/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 15:16:50 +00:00
Daniel Finneran
c367434798 Merge pull request #1068 from kube-vip/dependabot/go_modules/github.com/osrg/gobgp/v3-3.34.0
Bump github.com/osrg/gobgp/v3 from 3.33.0 to 3.34.0
2025-02-17 16:15:09 +01:00
Daniel Finneran
f1ca914e92 Merge pull request #1067 from kube-vip/dependabot/go_modules/golang.org/x/sys-0.30.0
Bump golang.org/x/sys from 0.29.0 to 0.30.0
2025-02-17 16:14:57 +01:00
Daniel Finneran
2763c7e417 Merge pull request #1072 from p-strusiewiczsurmacki-mobica/mask-fix
Fixed network mask setting function
2025-02-17 16:14:43 +01:00
Daniel Finneran
f9f0004d3f Merge pull request #1064 from kube-vip/resolver
Resolver, this will ensure that an address that is not an IP address is resolved first to determine the CIDR
2025-02-17 16:14:22 +01:00
Daniel Finneran
958eaefb40 Merge pull request #1062 from kube-vip/bgp_port
You can now specify the port as the fifth argument to peer config
2025-02-17 16:14:05 +01:00
Daniel Finneran
3752695000 Merge pull request #1060 from kube-vip/waitgroup
remove the waitgroups as we never wait
2025-02-17 16:13:38 +01:00
Daniel Finneran
d6eb7c4f26 Merge pull request #1059 from kube-vip/drop_all
drop all other capabilities
2025-02-17 16:13:21 +01:00
dependabot[bot]
b88769e109 Bump golang from 1.23.5-alpine3.20 to 1.24.0-alpine3.20
Bumps golang from 1.23.5-alpine3.20 to 1.24.0-alpine3.20.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 02:38:59 +00:00
dependabot[bot]
eddbcf0801 Bump alpine from 3.21.2 to 3.21.3
Bumps alpine from 3.21.2 to 3.21.3.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 02:38:56 +00:00
dependabot[bot]
886f183108 Bump github.com/cloudflare/ipvs from 0.10.2 to 0.10.3
Bumps [github.com/cloudflare/ipvs](https://github.com/cloudflare/ipvs) from 0.10.2 to 0.10.3.
- [Release notes](https://github.com/cloudflare/ipvs/releases)
- [Commits](https://github.com/cloudflare/ipvs/compare/v0.10.2...v0.10.3)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/ipvs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 02:15:06 +00:00
Patryk Strusiewicz-Surmacki
aba1d53699 Changed initConfig.Logging to be int32
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-02-13 19:36:39 +01:00
Patryk Strusiewicz-Surmacki
d05530e030 Fixed logging level configuration with command line argument
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-02-13 19:36:39 +01:00
Patryk Strusiewicz-Surmacki
7d52dbbbd5 Return error instead of logging warning
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-02-12 11:51:07 +01:00
Patryk Strusiewicz-Surmacki
53185bd58d Fixed ENV log level setting
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-02-10 21:24:48 +01:00
Patryk Strusiewicz-Surmacki
8997c4b1a8 Fixed network mask setting function
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-02-10 15:46:55 +01:00
dependabot[bot]
ce4665bbec Bump github.com/osrg/gobgp/v3 from 3.33.0 to 3.34.0
Bumps [github.com/osrg/gobgp/v3](https://github.com/osrg/gobgp) from 3.33.0 to 3.34.0.
- [Release notes](https://github.com/osrg/gobgp/releases)
- [Changelog](https://github.com/osrg/gobgp/blob/master/.goreleaser.yml)
- [Commits](https://github.com/osrg/gobgp/compare/v3.33.0...v3.34.0)

---
updated-dependencies:
- dependency-name: github.com/osrg/gobgp/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 02:10:52 +00:00
dependabot[bot]
e1ecca42c5 Bump golang.org/x/sys from 0.29.0 to 0.30.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/sys/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 02:10:48 +00:00
Dan Finneran
c6cb548763 This ensures a hostname is resolved if passed as the api address 2025-02-05 14:51:22 +00:00
Dan Finneran
b52c90f865 You can now specify the port as the fifth argument to peer config 2025-02-05 13:37:41 +00:00
Daniel Finneran
2ceba7f5ad Update ci.yaml
Remove the original control plane tests as everything is 1.29+ at this point
2025-02-05 12:32:22 +00:00
Daniel Finneran
c3121a97f1 Update README.md 2025-02-05 12:29:53 +00:00
Daniel Finneran
b7cc63bfd6 Merge pull request #1048 from kube-vip/dependabot/go_modules/k8s.io/api-0.32.1
Bump k8s.io/api from 0.32.0 to 0.32.1
2025-02-05 12:41:22 +01:00
Daniel Finneran
3a4d859457 Merge pull request #1047 from kube-vip/dependabot/go_modules/k8s.io/client-go-0.32.1
Bump k8s.io/client-go from 0.32.0 to 0.32.1
2025-02-05 12:41:02 +01:00
Dan Finneran
964b248108 remove the waitgroups as we never wait 2025-02-05 11:37:48 +00:00
Daniel Finneran
b6e87418a2 Merge pull request #1055 from kube-vip/dependabot/go_modules/go.etcd.io/etcd/client/v3-3.5.18
Bump go.etcd.io/etcd/client/v3 from 3.5.17 to 3.5.18
2025-02-05 12:01:27 +01:00
Dan Finneran
473fd6f4f8 drop all other capabilities 2025-02-05 09:58:14 +00:00
Daniel Finneran
c49afdb44d Merge branch 'main' into dependabot/go_modules/go.etcd.io/etcd/client/v3-3.5.18 2025-02-05 10:56:55 +01:00
dependabot[bot]
f0182fcb0c Bump k8s.io/api from 0.32.0 to 0.32.1
Bumps [k8s.io/api](https://github.com/kubernetes/api) from 0.32.0 to 0.32.1.
- [Commits](https://github.com/kubernetes/api/compare/v0.32.0...v0.32.1)

---
updated-dependencies:
- dependency-name: k8s.io/api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-05 09:55:47 +00:00
Daniel Finneran
7424df98a5 Merge pull request #1054 from kube-vip/dependabot/go_modules/go.etcd.io/etcd/client/pkg/v3-3.5.18
Bump go.etcd.io/etcd/client/pkg/v3 from 3.5.17 to 3.5.18
2025-02-05 10:55:39 +01:00
Daniel Finneran
b4c4cfd365 Merge pull request #1053 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.18.0
Bump anchore/sbom-action from 0.17.9 to 0.18.0
2025-02-05 10:55:27 +01:00
Daniel Finneran
7878be3847 Merge branch 'main' into dependabot/go_modules/k8s.io/client-go-0.32.1 2025-02-05 10:54:25 +01:00
Daniel Finneran
1dc75f9f21 Merge pull request #1046 from kube-vip/dependabot/go_modules/k8s.io/apimachinery-0.32.1
Bump k8s.io/apimachinery from 0.32.0 to 0.32.1
2025-02-05 10:53:34 +01:00
Daniel Finneran
cbe4cd150e Merge pull request #1043 from kube-vip/slog
Refactor to slog
2025-02-05 10:53:12 +01:00
Dan Finneran
99e1fb3fc6 linting 2025-02-05 09:38:20 +00:00
Dan Finneran
e1481db95c cleanup of messages 2025-02-05 09:24:39 +00:00
Daniel Finneran
efc7ba5646 Merge pull request #1045 from kube-vip/dependabot/docker/golang-1.23.5-alpine3.20
Bump golang from 1.23.4-alpine3.20 to 1.23.5-alpine3.20
2025-02-04 11:50:23 +00:00
Daniel Finneran
90552b73dc Merge pull request #1003 from p-strusiewiczsurmacki-mobica/feature/mp-bgp
Support for MP-BGP
2025-02-04 11:42:17 +00:00
Dan Finneran
81c0b4faf5 lint fix 2025-02-04 08:42:42 +00:00
Dan Finneran
95be514b8f fixes to slog formatting 2025-02-01 14:01:27 +00:00
Daniel Finneran
d289efc862 Merge branch 'main' into slog 2025-02-01 12:18:26 +00:00
Daniel Finneran
e388b2d1c9 Merge pull request #1056 from kube-vip/ndp_error
Ndp error
2025-02-01 12:12:52 +00:00
Dan Finneran
7ca0d79250 superflous error return 2025-02-01 11:49:09 +00:00
Dan Finneran
20b375770c reduced duplicated code and cleaned messages 2025-02-01 11:44:28 +00:00
Dan Finneran
6b3ada8402 NDP link-local messages 2025-02-01 11:07:48 +00:00
Dan Finneran
c12bb65b4b adds some testing for Cilium, not for CI 2025-01-29 14:27:05 +00:00
dependabot[bot]
2e0dddba60 Bump go.etcd.io/etcd/client/v3 from 3.5.17 to 3.5.18
Bumps [go.etcd.io/etcd/client/v3](https://github.com/etcd-io/etcd) from 3.5.17 to 3.5.18.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.17...v3.5.18)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/client/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 02:24:49 +00:00
dependabot[bot]
8e6767c606 Bump go.etcd.io/etcd/client/pkg/v3 from 3.5.17 to 3.5.18
Bumps [go.etcd.io/etcd/client/pkg/v3](https://github.com/etcd-io/etcd) from 3.5.17 to 3.5.18.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.17...v3.5.18)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/client/pkg/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 02:24:45 +00:00
dependabot[bot]
419051c762 Bump anchore/sbom-action from 0.17.9 to 0.18.0
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.17.9 to 0.18.0.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.17.9...v0.18.0)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 02:09:46 +00:00
Dan Finneran
9e432d1178 Fixes to swallowed errors 2025-01-24 09:20:55 +00:00
Patryk Strusiewicz-Surmacki
0832a7227c Added MP-BGP for IPv4 over IPv6 session and config flags
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2025-01-22 17:36:39 +01:00
Christopher Dziomba
3b16caa38c Messy prototype of MP-BGP with kube-vip 2025-01-21 12:10:07 +01:00
dependabot[bot]
1d4e57a754 Bump k8s.io/client-go from 0.32.0 to 0.32.1
Bumps [k8s.io/client-go](https://github.com/kubernetes/client-go) from 0.32.0 to 0.32.1.
- [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kubernetes/client-go/compare/v0.32.0...v0.32.1)

---
updated-dependencies:
- dependency-name: k8s.io/client-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 02:36:19 +00:00
dependabot[bot]
c36585ce98 Bump k8s.io/apimachinery from 0.32.0 to 0.32.1
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.32.0 to 0.32.1.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.32.0...v0.32.1)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 02:36:11 +00:00
dependabot[bot]
af1280be1a Bump golang from 1.23.4-alpine3.20 to 1.23.5-alpine3.20
Bumps golang from 1.23.4-alpine3.20 to 1.23.5-alpine3.20.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 02:32:12 +00:00
Dan Finneran
c01fb8fca7 fix_spelling 2025-01-16 18:39:02 +00:00
Dan Finneran
1d6d860153 set default loglevel 2025-01-16 17:46:11 +00:00
Dan Finneran
2136e69a82 Big refactor to log/slog 2025-01-16 17:45:09 +00:00
Daniel Finneran
cb438d289b Merge pull request #1040 from kube-vip/restart_on_modification
Restart on modification
2025-01-16 12:50:20 +00:00
Dan Finneran
ded08ddf5c Fixing nits 2025-01-16 10:45:36 +00:00
Dan Finneran
79b24875ec le sigh 2025-01-16 09:46:18 +00:00
Daniel Finneran
b423cbdb89 Merge branch 'main' into restart_on_modification 2025-01-16 09:26:52 +00:00
Daniel Finneran
600c1db24b Merge pull request #1036 from kube-vip/logic_clean
This removes some weird pointless code
2025-01-16 09:25:56 +00:00
Dan Finneran
b2979be25c Linting fixes 2025-01-15 11:19:09 +00:00
Dan Finneran
462b511b9f This change means that a modification to an existing LB results in updating the settings. 2025-01-15 11:10:19 +00:00
Daniel Finneran
19e660d4a6 Update Makefile 2025-01-14 13:29:35 +00:00
Daniel Finneran
cfa11d1a88 Merge pull request #1037 from kube-vip/dependabot/go_modules/github.com/onsi/ginkgo/v2-2.22.2
Bump github.com/onsi/ginkgo/v2 from 2.22.1 to 2.22.2
2025-01-14 13:29:16 +00:00
Daniel Finneran
bf283f1252 Merge pull request #1038 from kube-vip/dependabot/docker/alpine-3.21.2
Bump alpine from 3.21.0 to 3.21.2
2025-01-14 13:29:00 +00:00
dependabot[bot]
839b860eee Bump alpine from 3.21.0 to 3.21.2
Bumps alpine from 3.21.0 to 3.21.2.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 02:54:42 +00:00
dependabot[bot]
e134e5682e Bump github.com/onsi/ginkgo/v2 from 2.22.1 to 2.22.2
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.22.1 to 2.22.2.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.22.1...v2.22.2)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 02:26:18 +00:00
Dan Finneran
ebc4cdccb9 more_lint 2025-01-12 13:02:08 +00:00
Dan Finneran
baed70dbfc fix_linting 2025-01-12 12:53:05 +00:00
Dan Finneran
b8aff1d348 fix_lint 2025-01-12 12:52:58 +00:00
Dan Finneran
aa5f0cc267 This removes some weird pointless code 2025-01-12 11:36:30 +00:00
Daniel Finneran
316c3bbdd4 Merge pull request #1034 from murphp15/feature/release_the_lock_if_you_cant_advertise_service
release lock if unable to sync service correctly.
2025-01-10 16:17:37 +00:00
paul.murphy
dbc02485d9 release lock if unable to sync service correctly. 2025-01-10 15:54:08 +00:00
paul.murphy
6442ce26fc release lock if unable to sync service correctly. 2025-01-10 15:37:41 +00:00
Paul Murphy
ccd1137606 Merge branch 'kube-vip:main' into feature/release_the_lock_if_you_cant_advertise_service 2025-01-10 14:47:32 +00:00
Daniel Finneran
96d79774b5 Merge pull request #1035 from kube-vip/print_build_info
by default print build
2025-01-10 14:22:12 +00:00
Dan Finneran
829a1fced6 by default print build 2025-01-10 14:20:35 +00:00
paul.murphy
0c5dd3c890 release lock if unable to sync service correctly. 2025-01-10 10:49:51 +00:00
paul.murphy
51aad755c3 release lock if unable to sync service correctly. 2025-01-10 10:49:34 +00:00
paul.murphy
635950e329 release lock if unable to sync service correctly. 2025-01-10 10:45:33 +00:00
paul.murphy
7eb93bec0b Merge branch 'main' into feature/release_the_lock_if_you_cant_advertise_service 2025-01-10 10:39:19 +00:00
paul.murphy
cf8384fb1f release lock if unable to sync service correctly. 2025-01-10 10:36:45 +00:00
Daniel Finneran
6da951056c Merge pull request #1030 from kube-vip/services_fix
Ensure watcher is restarted as expected
2025-01-09 11:35:41 +00:00
Daniel Finneran
a50e476d7e Merge pull request #1029 from kube-vip/watcher-fix
Fixes an issue with watchers having an old reference
2025-01-09 11:21:37 +00:00
Dan Finneran
329e0940da Ensure watcher is restarted as expected 2025-01-09 11:05:03 +00:00
Dan Finneran
7a43646e62 Fixes an issue with watchers having an old reference 2025-01-08 18:27:30 +00:00
Daniel Finneran
dd8f2d13d2 Update Makefile 2025-01-06 13:47:49 +00:00
Daniel Finneran
f119c890a1 Merge pull request #1024 from kube-vip/small_fix
Fix linting issue
2025-01-06 13:46:49 +00:00
Dan Finneran
714b80225f Fix linting issue 2025-01-06 10:52:48 +00:00
Daniel Finneran
49297980b0 Merge pull request #923 from cdtzabra/main
Ensuring nodelabel will be always present
2025-01-06 10:51:31 +00:00
Daniel Finneran
e7beb9c9c3 Merge pull request #1012 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.17.9
Bump anchore/sbom-action from 0.17.5 to 0.17.9
2025-01-06 10:23:02 +00:00
dependabot[bot]
5e66a62c32 Bump anchore/sbom-action from 0.17.5 to 0.17.9
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.17.5 to 0.17.9.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.17.5...v0.17.9)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-06 09:54:08 +00:00
Daniel Finneran
fbbf83d9d0 Merge pull request #1008 from kube-vip/dependabot/docker/golang-1.23.4-alpine3.20
Bump golang from 1.23.2-alpine3.20 to 1.23.4-alpine3.20
2025-01-06 09:51:24 +00:00
Daniel Finneran
7e244084e1 Merge pull request #1022 from kube-vip/deps
update deps
2025-01-06 09:50:40 +00:00
Dan Finneran
089ad123b6 losing hope 2025-01-05 17:12:35 +00:00
Dan Finneran
fdc50efc86 clutching at straws now 2025-01-05 17:10:24 +00:00
Dan Finneran
7644cb720e attempt 2 2025-01-05 17:05:48 +00:00
Dan Finneran
299c9e1ebb updatae go.mod 2025-01-05 16:02:06 +00:00
Dan Finneran
b82b733dde CI fix 2025-01-05 15:58:58 +00:00
Dan Finneran
e339d12b40 update deps 2025-01-05 15:53:34 +00:00
dependabot[bot]
3ea17b134d Bump golang from 1.23.2-alpine3.20 to 1.23.4-alpine3.20
Bumps golang from 1.23.2-alpine3.20 to 1.23.4-alpine3.20.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-05 15:30:03 +00:00
Daniel Finneran
ea410e7490 Merge pull request #1007 from kube-vip/dependabot/docker/alpine-3.21.0
Bump alpine from 3.20.3 to 3.21.0
2025-01-05 15:29:04 +00:00
Daniel Finneran
430efae598 Merge pull request #1019 from kube-vip/egress_annotations
Adds a bunch of flexability around egress
2025-01-05 15:27:29 +00:00
Dan Finneran
c96cdb6cd2 test fix 2025-01-05 15:03:53 +00:00
Dan Finneran
ffe2e9c808 Various fixes to CI 2025-01-05 14:59:51 +00:00
Dan Finneran
545f9a4a47 fix to modules and workflow 2024-12-20 16:38:56 +00:00
Dan Finneran
2555dd8101 Adds a bunch of flexability around egress 2024-12-20 16:25:06 +00:00
dependabot[bot]
f7b4ab5b42 Bump alpine from 3.20.3 to 3.21.0
Bumps alpine from 3.20.3 to 3.21.0.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 02:39:44 +00:00
Daniel Finneran
4c70d87381 Update Makefile 2024-11-20 09:08:47 +00:00
Dan Finneran
894e56458f remove commented out code 2024-11-15 17:07:51 +01:00
Dan Finneran
de5659e7b2 Services aren't removed when there are no endpoints. 2024-11-15 17:07:51 +01:00
Marcel Fest
e4b0b2a71f Use the new release version in Makefile
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2024-11-11 08:56:48 +01:00
Daniel Finneran
dd06c3fb82 Merge pull request #987 from p-strusiewiczsurmacki-mobica/rwclient-ipvs-fix
Fixed null pointer exception when IPVS loadbalancer is used
2024-11-04 11:31:28 +00:00
Patryk Strusiewicz-Surmacki
8ee952cf3e Fixed null pointer exception when IPVS loadbalancer is used
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-11-04 11:50:34 +01:00
Daniel Finneran
57bcea9646 Merge pull request #984 from kube-vip/fix/retry-watch
initialize the config for the rwclient separately
2024-11-01 11:33:00 +00:00
Cellebyte
907696a4a3 initialize the config for the rwclient separately
Signed-off-by: Cellebyte <marcel.fest@live.de>
2024-11-01 10:50:09 +01:00
Daniel Finneran
78cdf8d5b6 Merge pull request #981 from kube-vip/New-release-0.8.5 2024-10-31 12:59:10 +00:00
Daniel Finneran
5a7edbe825 Merge pull request #976 from l0wl3vel/upnp-gateway-ip 2024-10-31 12:47:35 +00:00
Daniel Finneran
de8300a40a Merge pull request #961 from p-strusiewiczsurmacki-mobica/auto-svc-subnet 2024-10-31 12:46:10 +00:00
Patryk Strusiewicz-Surmacki
73e6ade0ea Added auto subnet discovery for services in ARP mode
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-31 10:31:39 +01:00
Benjamin Ritter
4def2c784e Update Gateway IPs on UPNP refresh
Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-10-30 17:51:00 +01:00
Benjamin Ritter
1c8ce223d7 Reduce UPNP refresh interval to 5 minutes
Reduces impact on availibility when the
gateway external IP changes

Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-10-30 17:51:00 +01:00
Benjamin Ritter
02d00bf99c Annotate UPNP Gateway IPs on the service
Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-10-30 17:51:00 +01:00
Daniel Finneran
4433243fa6 Update Makefile
This PR should be the last merged PR for the 0.8.5 release, which should also be the last of the `0.8.x` releases.
2024-10-30 15:21:43 +00:00
Daniel Finneran
a8ae976bb2 Merge pull request #968 from kube-vip/dependabot/go_modules/github.com/cloudflare/ipvs-0.10.2
Bump github.com/cloudflare/ipvs from 0.10.1 to 0.10.2
2024-10-30 14:38:40 +00:00
Daniel Finneran
1559b21cfb Merge pull request #954 from l0wl3vel/upnp-igd2
Use IGD:2 and Pinhole Port Forwarding
2024-10-30 14:37:34 +00:00
Daniel Finneran
f55aed4382 Merge pull request #979 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.17.5
Bump anchore/sbom-action from 0.17.3 to 0.17.5
2024-10-30 14:36:21 +00:00
Daniel Finneran
cc12fcb0f6 Merge pull request #970 from p-strusiewiczsurmacki-mobica/egress-rules-fix
Egress rules deletion fix
2024-10-30 14:36:00 +00:00
Patryk Strusiewicz-Surmacki
95dfc47e47 Added info on egress protocol
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-30 14:31:41 +01:00
Patryk Strusiewicz-Surmacki
0bf38f57f7 Fixed logging issues
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
7f4116417c Fixed typo
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
5763400c15 Added safeguards for IP family
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
6b136efff3 Added changes to CIDR discovery from auto-svc-subnet branch
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
1f089fe71c Added routing table ID fix
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
1bc26b5827 Fixed e2e
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
40674a12b6 Fixed route deleting issue in table mode
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
ef2f6ebaa0 Fixed auto CIDR discovery priority
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-29 11:46:24 +01:00
Patryk Strusiewicz-Surmacki
625030dfb6 Fixed retry-watcher timeout issue (#974)
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-28 20:14:05 +01:00
Benjamin Ritter
779fc5641f Add context to UPNP logging
Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-10-28 15:24:18 +01:00
dependabot[bot]
971e794639 Bump anchore/sbom-action from 0.17.3 to 0.17.5
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.17.3 to 0.17.5.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.17.3...v0.17.5)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 02:36:00 +00:00
Patryk Strusiewicz-Surmacki
89c6002f92 Fixed NDP responder (#973)
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-27 22:27:55 +01:00
Benjamin Ritter
6b0e45da96 Clean up UPNP client handling
Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-10-27 20:50:32 +01:00
Benjamin Ritter
7bde7a4845 feat: Add perpetual UPNP Forward refreshing
Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-10-24 21:54:44 +02:00
Benjamin Ritter
eda0bd69b3 Use IGD:2 and Pinhole Port Forwarding
Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-10-24 21:54:30 +02:00
dependabot[bot]
2a56bdb0c8 Bump github.com/cloudflare/ipvs from 0.10.1 to 0.10.2
Bumps [github.com/cloudflare/ipvs](https://github.com/cloudflare/ipvs) from 0.10.1 to 0.10.2.
- [Release notes](https://github.com/cloudflare/ipvs/releases)
- [Commits](https://github.com/cloudflare/ipvs/compare/v0.10.1...v0.10.2)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/ipvs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-21 02:20:18 +00:00
Daniel Finneran
de0375610d Merge pull request #965 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.17.3
Bump anchore/sbom-action from 0.17.2 to 0.17.3
2024-10-18 11:18:27 +02:00
Daniel Finneran
382024104a Merge pull request #964 from kube-vip/dependabot/go_modules/github.com/spf13/cobra-1.8.1
Bump github.com/spf13/cobra from 1.8.0 to 1.8.1
2024-10-18 11:18:08 +02:00
dependabot[bot]
e896b55c25 Bump anchore/sbom-action from 0.17.2 to 0.17.3
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.17.2 to 0.17.3.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.17.2...v0.17.3)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 02:50:27 +00:00
dependabot[bot]
53ce87755a Bump github.com/spf13/cobra from 1.8.0 to 1.8.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 02:31:14 +00:00
Daniel Finneran
750f78164f Merge pull request #918 from kube-vip/dependabot/go_modules/k8s.io/klog/v2-2.130.1
Bump k8s.io/klog/v2 from 2.120.1 to 2.130.1
2024-10-10 09:41:30 +01:00
Daniel Finneran
bf6cb8e39e Merge pull request #887 from kube-vip/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-10-10 09:39:55 +01:00
Daniel Finneran
f58e110057 Update Makefile 2024-10-10 09:39:28 +01:00
dependabot[bot]
98db46c817 Bump k8s.io/klog/v2 from 2.120.1 to 2.130.1
Bumps [k8s.io/klog/v2](https://github.com/kubernetes/klog) from 2.120.1 to 2.130.1.
- [Release notes](https://github.com/kubernetes/klog/releases)
- [Changelog](https://github.com/kubernetes/klog/blob/main/RELEASE.md)
- [Commits](https://github.com/kubernetes/klog/compare/v2.120.1...v2.130.1)

---
updated-dependencies:
- dependency-name: k8s.io/klog/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-10 08:28:26 +00:00
Daniel Finneran
da9b440f1e Merge pull request #939 from kube-vip/dependabot/docker/alpine-3.20.3
Bump alpine from 3.20.0 to 3.20.3
2024-10-10 09:27:48 +01:00
dependabot[bot]
a18039373d Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-10 08:27:05 +00:00
Daniel Finneran
a9753fafd2 Merge pull request #960 from kube-vip/dependabot/docker/golang-1.23.2-alpine3.20
Bump golang from 1.23.1-alpine3.20 to 1.23.2-alpine3.20
2024-10-10 09:26:45 +01:00
Daniel Finneran
8aa0a6b9e0 Merge pull request #958 from p-strusiewiczsurmacki-mobica/control-plane-table-e2e
Added e2e tests for control-plane in routing table mode, fixed IPv6 issue
2024-10-07 17:00:48 +01:00
dependabot[bot]
8f16021e4c Bump golang from 1.23.1-alpine3.20 to 1.23.2-alpine3.20
Bumps golang from 1.23.1-alpine3.20 to 1.23.2-alpine3.20.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 02:27:00 +00:00
Patryk Strusiewicz-Surmacki
456d52d507 Added e2e tests for control-plane in routing table mode and fixed IPv6 issue
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-03 17:11:57 +02:00
Daniel Finneran
4da24fb64f Merge pull request #957 from p-strusiewiczsurmacki-mobica/control-plane-table-mode
Routing Table mode for Control Plane
2024-10-03 11:27:15 +02:00
Daniel Finneran
c27044b521 Merge pull request #952 from l0wl3vel/upnp-forward-multiple-ports
UPNP: Assume that multiple ports can exist on a service
2024-10-02 13:23:20 +02:00
Patryk Strusiewicz-Surmacki
810cf89f97 Routing Table mode for Control Plane
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-01 18:00:59 +02:00
Patryk Strusiewicz-Surmacki
d65af2c84f Fixed lastKnownGoodEndpoint not being set when in non-leader-election mode (#955)
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-10-01 14:29:42 +02:00
Daniel Finneran
d30fda1d9d Merge pull request #956 from tenyo/lo-iface-scope
Add loInterfaceGlobalScope flag for enabling global scope for vip lo interface
2024-09-30 14:24:42 +02:00
tenyo
b6d4aa1632 add loInterfaceGlobalScope flag for enabling global scope for vip lo interface
Signed-off-by: tenyo <tgrozev@equinix.com>
2024-09-27 20:21:51 +00:00
Benjamin Ritter
b024d04d91 Assume that multiple ports can exist on a service
Fixes UPNP exposing only the port with the lowest numeric value. #951

Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-09-24 16:06:43 +02:00
Daniel Finneran
6cdc7a86c4 Merge pull request #950 from l0wl3vel/upnp-per-service
Enable UPNP Port forwarding per service
2024-09-24 08:47:26 +01:00
Benjamin Ritter
c6f1c740fd feat: Gate UPNP port forwarding behind kube-vip.io/forwardUPNP annotation
Signed-off-by: Benjamin Ritter <Benjamin.Ritter@protonmail.com>
2024-09-22 10:31:32 +02:00
Daniel Finneran
95c43472e9 Update Makefile 2024-09-09 13:34:45 +01:00
dependabot[bot]
c8a222e244 Bump alpine from 3.20.0 to 3.20.3
Bumps alpine from 3.20.0 to 3.20.3.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 08:30:04 +00:00
Daniel Finneran
37abb44a64 Merge pull request #940 from kube-vip/dependabot/docker/golang-1.23.1-alpine3.20
Bump golang from 1.22.6-alpine3.20 to 1.23.1-alpine3.20
2024-09-09 09:25:34 +01:00
dependabot[bot]
b4e6725a1a Bump golang from 1.22.6-alpine3.20 to 1.23.1-alpine3.20
Bumps golang from 1.22.6-alpine3.20 to 1.23.1-alpine3.20.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 02:29:55 +00:00
Daniel Finneran
b6410d9b4d Merge pull request #927 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.17.2
Bump anchore/sbom-action from 0.17.0 to 0.17.2
2024-09-06 16:11:49 +03:00
Daniel Finneran
4bdfeffc96 Merge pull request #937 from thebsdbox/ipv6_egress
This adds ipv6 egress & test
2024-09-06 16:11:35 +03:00
Daniel Finneran
a32804dfee Update pkg/vip/util.go
I like it

Co-authored-by: Patryk Strusiewicz-Surmacki <137421299+p-strusiewiczsurmacki-mobica@users.noreply.github.com>
2024-09-06 12:31:03 +01:00
Dan Finneran
b24ce18d55 Fix test
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-09-05 12:40:24 +00:00
Dan Finneran
5512a08aac restored the exact behaviour for IPv4
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-09-05 12:22:41 +00:00
Dan Finneran
f438367d6e This adds ipv6 egress & test
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-09-05 12:08:39 +00:00
cdtzabra
62a724122c Update node_labeling.go 2024-09-03 12:19:51 +02:00
Daniel Finneran
fb86cb8687 Merge pull request #929 from linsite/cleanup-makefile
clean up Makefile
2024-09-02 15:59:31 +03:00
linmaolin
29a0dc6c07 clean up Makefile
fmt target is stale, just removing it, also simplify target is not working under 1.21 toolchain,
update it as well.

Signed-off-by: Lin Lin <linlin152@foxmail.com>
2024-08-28 11:57:40 +08:00
dependabot[bot]
82f3c6a491 Bump anchore/sbom-action from 0.17.0 to 0.17.2
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.17.0 to 0.17.2.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.17.0...v0.17.2)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-26 02:53:48 +00:00
Daniel Finneran
fbb2746d07 Merge pull request #926 from p-strusiewiczsurmacki-mobica/svc-auto-interface
Auto network interface discovery for services
2024-08-23 17:55:26 +01:00
Patryk Strusiewicz-Surmacki
185d8bc484 Auto network interface discovery for services
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-08-23 15:15:11 +02:00
cdtzabra
db88185c40 Merge pull request #1 from cdtzabra/cdtzabra-patch-1
Ensuring nodelabel will be always present
2024-08-21 18:24:01 +02:00
cdtzabra
611cb0288f Ensuring nodelabel will be always present 2024-08-21 18:22:57 +02:00
Daniel Finneran
b18bdd0fb9 Merge pull request #872 from kube-vip/dependabot/go_modules/github.com/stretchr/testify-1.9.0
Bump github.com/stretchr/testify from 1.8.4 to 1.9.0
2024-08-15 12:46:28 +01:00
Daniel Finneran
8ef0d459c0 Merge pull request #902 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.17.0
Bump anchore/sbom-action from 0.16.0 to 0.17.0
2024-08-12 15:16:44 +02:00
Daniel Finneran
41bc78bc8f Merge pull request #915 from kube-vip/dependabot/docker/golang-1.22.6-alpine3.20
Bump golang from 1.22.4-alpine3.20 to 1.22.6-alpine3.20
2024-08-12 15:16:30 +02:00
Daniel Finneran
05984b9860 Merge pull request #910 from M0NsTeRRR/fix/ipv6-port-join
fix: ipv6 host and port join
2024-08-12 15:16:13 +02:00
dependabot[bot]
82b1e8536b Bump golang from 1.22.4-alpine3.20 to 1.22.6-alpine3.20
Bumps golang from 1.22.4-alpine3.20 to 1.22.6-alpine3.20.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 02:26:32 +00:00
Daniel Finneran
425cb92dee Merge pull request #904 from M0NsTeRRR/main
fix: set vip_cidr default value
2024-08-07 10:37:53 +02:00
Ludovic Ortega
9470edbea9 fix: ipv6 host and port join
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2024-08-07 00:48:59 +02:00
Ludovic Ortega
a5d6846608 fix: set vip_cidr default value
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2024-07-23 21:15:12 +02:00
Daniel Finneran
04ce471366 Update Makefile 2024-07-23 09:27:46 +01:00
Daniel Finneran
6c090701ea Merge pull request #897 from TilBlechschmidt/bug/dhcp-renew-failure
Change service interface to `macvlan` when using DHCP
2024-07-23 09:25:41 +01:00
dependabot[bot]
87fd49ac98 Bump anchore/sbom-action from 0.16.0 to 0.17.0
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.16.0 to 0.17.0.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 02:47:10 +00:00
Til Blechschmidt
a0a19ea3fa Change service interface to macvlan when using DHCP
When using DHCP for LB services, the resulting IP was assigned to the
`serviceInterface` while most (not all) DHCP communication was done
through a per-service `macvlan` interface that is created on demand.

This has been the cause of DHCP lease renewal failures for some users,
as the response packets from the server never reached kube-vip. For more
details, see issue #871.

To remedy this, the primary interface used for a service is now being
set to the `macvlan`. Consequently, the IP address is also assigned to
it.

As a result, all traffic related to an LB service (DHCP or not) is now
originating from and sent to the per-service `macvlan` interface that
is tied to the user-configured `serviceInterface`.

Fixes #871

Signed-off-by: Til Blechschmidt <me@tibl.dev>
2024-07-19 12:26:43 +02:00
Daniel Finneran
a571d0a9c2 Merge pull request #894 from thebsdbox/manifest_fix
Manifest fix
2024-07-11 09:14:40 +01:00
Dan Finneran
6f267462bb linting fixes
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-07-11 08:03:51 +00:00
Dan Finneran
a52e9a1818 adds CI test
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-07-10 15:24:32 +00:00
Dan Finneran
e883c1ea5d fixes manifest generation
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-07-10 15:15:24 +00:00
Marcel Fest
59951cbd4b [Bugfix] Parse the routingTableID correctly (#889)
* Parse the routingTableID correctly [rtnetlink.h](55027e6899/include/uapi/linux/rtnetlink.h\#L354C15-L354C25) says RT_TABLE_MAX=0xFFFFFFFF

Signed-off-by: Marcel Fest <marcel.fest@telekom.de>

* Better error handling for int check

Signed-off-by: Marcel Fest <marcel.fest@telekom.de>

* Better error message + typo fix

Signed-off-by: Marcel Fest <marcel.fest@telekom.de>

* Use MaxInt instead of MaxUint

Signed-off-by: Marcel Fest <marcel.fest@telekom.de>

---------

Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2024-06-26 12:29:07 +02:00
Daniel Finneran
9abce4a215 Update Makefile 2024-06-21 15:31:03 +01:00
Daniel Finneran
0b33aa64fc Merge pull request #884 from thebsdbox/go_security
bump golang for security fix
2024-06-21 15:30:47 +01:00
Dan Finneran
57980bd23f bump golang for security fix
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-06-21 14:18:58 +00:00
Daniel Finneran
69f4c389c5 Merge pull request #883 from thebsdbox/prometheus_fix
This allows prometheus configuration to be specified in the cli
2024-06-21 15:18:28 +01:00
Dan Finneran
f56c9b7480 This allows prometheus configuration to be specified in the cli
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-06-21 13:47:14 +00:00
dependabot[bot]
f8af9c37af Bump github.com/stretchr/testify from 1.8.4 to 1.9.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-10 02:59:36 +00:00
Daniel Finneran
c16f634cf7 Merge pull request #869 from kube-vip/dependabot/go_modules/github.com/osrg/gobgp/v3-3.27.0
Bump github.com/osrg/gobgp/v3 from 3.25.0 to 3.27.0
2024-06-05 11:06:15 +01:00
dependabot[bot]
5df534a1e7 Bump github.com/osrg/gobgp/v3 from 3.25.0 to 3.27.0
Bumps [github.com/osrg/gobgp/v3](https://github.com/osrg/gobgp) from 3.25.0 to 3.27.0.
- [Release notes](https://github.com/osrg/gobgp/releases)
- [Changelog](https://github.com/osrg/gobgp/blob/master/.goreleaser.yml)
- [Commits](https://github.com/osrg/gobgp/compare/v3.25.0...v3.27.0)

---
updated-dependencies:
- dependency-name: github.com/osrg/gobgp/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-04 13:20:19 +00:00
Daniel Finneran
ee535938b4 Merge pull request #861 from kube-vip/dependabot/go_modules/github.com/onsi/ginkgo/v2-2.19.0
Bump github.com/onsi/ginkgo/v2 from 2.17.2 to 2.19.0
2024-06-04 14:19:14 +01:00
Daniel Finneran
0df8a0dff4 Merge pull request #862 from kube-vip/dependabot/docker/alpine-3.20.0
Bump alpine from 3.19.1 to 3.20.0
2024-06-04 14:17:22 +01:00
Daniel Finneran
0e2a10535b Merge pull request #860 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.16.0
Bump anchore/sbom-action from 0.15.11 to 0.16.0
2024-06-04 14:17:08 +01:00
Daniel Finneran
b5d9ede6f8 Merge pull request #855 from p-strusiewiczsurmacki-mobica/lbclass-filtering
New service class filtering behavior
2024-06-04 14:16:51 +01:00
Daniel Finneran
94bad6d53b Merge pull request #868 from murphp15/main
Small correction to docs on retryPeriod property
2024-06-04 14:16:38 +01:00
Paul Murphy
927bb95ead Update kube-vip.go 2024-06-04 13:33:20 +01:00
Paul Murphy
7b96ebc7d4 Update config_types.go 2024-06-04 12:34:41 +01:00
Paul Murphy
5b05f365f1 Update kube-vip.go 2024-06-04 12:33:19 +01:00
dependabot[bot]
cd3d8a592c Bump alpine from 3.19.1 to 3.20.0
Bumps alpine from 3.19.1 to 3.20.0.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 02:42:31 +00:00
dependabot[bot]
e69fadf19b Bump github.com/onsi/ginkgo/v2 from 2.17.2 to 2.19.0
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.17.2 to 2.19.0.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.17.2...v2.19.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 02:35:13 +00:00
dependabot[bot]
5ded2d7574 Bump anchore/sbom-action from 0.15.11 to 0.16.0
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.15.11 to 0.16.0.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.15.11...v0.16.0)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-27 02:12:43 +00:00
Marcel Fest
c683bb879e Merge pull request #854 from kube-vip/hotfix/route-deletion
[Hotfix] Routes get not deleted when kube-vip is running without LeaderElection/ServiceElection in Table Mode
2024-05-17 15:53:57 +02:00
Daniel Finneran
392583d460 Merge pull request #853 from M0NsTeRRR/main
feat: generate VIPCIDR if not value is set
2024-05-17 14:15:23 +01:00
Patryk Strusiewicz-Surmacki
abf49f2fa6 New service class filtering behavior
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-05-17 14:45:30 +02:00
Marcel Fest
25c2532e74 when in table mode we always want to clean routes
Signed-off-by: Marcel Fest <marcel.fest@telekom.de>
2024-05-17 12:30:53 +02:00
Ludovic Ortega
28918e839f feat: generate VIPCIDR if not value is set
Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2024-05-16 22:42:53 +02:00
Daniel Finneran
1620e4c890 Merge pull request #846 from testwill/close_file
fix: close file
2024-05-16 09:19:24 +01:00
guoguangwu
c8bdf61a87 fix: close file
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
2024-05-15 19:13:41 +08:00
Daniel Finneran
e9835aa981 Merge pull request #847 from kube-vip/dependabot/docker/golang-1.22.3-alpine3.18
Bump golang from 1.22.2-alpine3.18 to 1.22.3-alpine3.18
2024-05-15 11:59:21 +01:00
dependabot[bot]
ca3dad817d Bump golang from 1.22.2-alpine3.18 to 1.22.3-alpine3.18
Bumps golang from 1.22.2-alpine3.18 to 1.22.3-alpine3.18.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-13 02:39:51 +00:00
Daniel Finneran
cb29d7cb17 Merge pull request #840 from kube-vip/dependabot/go_modules/github.com/onsi/gomega-1.33.1
Bump github.com/onsi/gomega from 1.32.0 to 1.33.1
2024-05-09 08:49:20 +01:00
Daniel Finneran
0fa7cbdab8 Merge pull request #834 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.15.11
Bump anchore/sbom-action from 0.15.10 to 0.15.11
2024-05-09 08:49:08 +01:00
dependabot[bot]
0038c27fec Bump github.com/onsi/gomega from 1.32.0 to 1.33.1
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.32.0 to 1.33.1.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.32.0...v1.33.1)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-08 14:01:32 +00:00
Daniel Finneran
4c19a3370c Merge pull request #826 from kube-vip/dependabot/go_modules/github.com/prometheus/client_golang-1.19.0
Bump github.com/prometheus/client_golang from 1.18.0 to 1.19.0
2024-05-08 14:59:30 +01:00
Daniel Finneran
25abee70cd Merge pull request #833 from kube-vip/dependabot/go_modules/github.com/onsi/ginkgo/v2-2.17.2
Bump github.com/onsi/ginkgo/v2 from 2.15.0 to 2.17.2
2024-05-08 14:58:37 +01:00
Daniel Finneran
057a32d191 Merge pull request #838 from p-strusiewiczsurmacki-mobica/route-deletion-fix
Fixed route deletion when route is used by more than one service
2024-05-08 14:58:21 +01:00
Daniel Finneran
b33a7b6d32 Merge pull request #837 from starbops/fix-835
fix: set service's uid in activeService map for inactive services
2024-05-08 14:57:58 +01:00
Patryk Strusiewicz-Surmacki
ba0c9282e7 Fixed route deletion when route is used by more than one service
Signed-off-by: Patryk Strusiewicz-Surmacki <patryk-pawel.strusiewicz-surmacki@external.telekom.de>
2024-04-30 14:44:26 +02:00
Zespre Schmidt
a431daab42 fix: set service's uid in activeService map for inactive services
Signed-off-by: Zespre Schmidt <starbops@zespre.com>
2024-04-30 16:31:10 +08:00
dependabot[bot]
54e4334b9f Bump anchore/sbom-action from 0.15.10 to 0.15.11
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.15.10 to 0.15.11.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.15.10...v0.15.11)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 02:55:01 +00:00
dependabot[bot]
6545a5aaab Bump github.com/onsi/ginkgo/v2 from 2.15.0 to 2.17.2
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.15.0 to 2.17.2.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.15.0...v2.17.2)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 02:13:32 +00:00
Daniel Finneran
2872256a89 Merge pull request #832 from ShivanshVij/main
fix: unecessary string modification
2024-04-25 08:26:35 +01:00
Shivansh Vij
a1ae304d07 fix: unecessary string modification
Signed-off-by: Shivansh Vij <shivanshvij@loopholelabs.io>
2024-04-24 12:32:27 -07:00
dependabot[bot]
c90d2043f0 Bump github.com/prometheus/client_golang from 1.18.0 to 1.19.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-22 02:47:11 +00:00
Daniel Finneran
722c47fc08 Update Makefile 2024-04-18 18:11:51 +01:00
Daniel Finneran
27fae664fe Merge pull request #821 from kube-vip/dependabot/go_modules/go.etcd.io/etcd/client/v3-3.5.13
Bump go.etcd.io/etcd/client/v3 from 3.5.11 to 3.5.13
2024-04-18 15:12:18 +01:00
Daniel Finneran
7349576e5b Merge pull request #820 from thebsdbox/bgp_err
Errors are being ignored
2024-04-18 14:59:08 +01:00
dependabot[bot]
1ab5047941 Bump go.etcd.io/etcd/client/v3 from 3.5.11 to 3.5.13
Bumps [go.etcd.io/etcd/client/v3](https://github.com/etcd-io/etcd) from 3.5.11 to 3.5.13.
- [Release notes](https://github.com/etcd-io/etcd/releases)
- [Commits](https://github.com/etcd-io/etcd/compare/v3.5.11...v3.5.13)

---
updated-dependencies:
- dependency-name: go.etcd.io/etcd/client/v3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-18 13:52:10 +00:00
Dan Finneran
950264e403 lint fixes
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-04-18 13:44:08 +00:00
Dan Finneran
0686b92376 Errors are being ignored
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-04-18 13:38:38 +00:00
Daniel Finneran
b7ebb21dc0 Merge pull request #796 from kube-vip/dependabot/go_modules/github.com/onsi/gomega-1.32.0
Bump github.com/onsi/gomega from 1.30.0 to 1.32.0
2024-04-18 14:22:50 +01:00
Daniel Finneran
ab904e2e18 Merge pull request #819 from thebsdbox/example
Examples and fixes to annotation interface
2024-04-18 14:22:37 +01:00
Dan Finneran
a5d4d0ca98 Examples and fixes to annotation interface
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-04-18 13:02:21 +00:00
dependabot[bot]
7fb301d3b3 Bump github.com/onsi/gomega from 1.30.0 to 1.32.0
Bumps [github.com/onsi/gomega](https://github.com/onsi/gomega) from 1.30.0 to 1.32.0.
- [Release notes](https://github.com/onsi/gomega/releases)
- [Changelog](https://github.com/onsi/gomega/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/gomega/compare/v1.30.0...v1.32.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/gomega
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-18 10:01:12 +00:00
Daniel Finneran
aa4da2f72a Merge pull request #816 from thebsdbox/interface_annotation
Adds annotation support for service interfaces.
2024-04-18 11:00:33 +01:00
Daniel Finneran
5dc91e6b3b Merge pull request #806 from kube-vip/dependabot/go_modules/github.com/osrg/gobgp/v3-3.25.0
Bump github.com/osrg/gobgp/v3 from 3.22.0 to 3.25.0
2024-04-18 10:58:29 +01:00
dependabot[bot]
658ca8697e Bump github.com/osrg/gobgp/v3 from 3.22.0 to 3.25.0
Bumps [github.com/osrg/gobgp/v3](https://github.com/osrg/gobgp) from 3.22.0 to 3.25.0.
- [Release notes](https://github.com/osrg/gobgp/releases)
- [Changelog](https://github.com/osrg/gobgp/blob/master/.goreleaser.yml)
- [Commits](https://github.com/osrg/gobgp/compare/v3.22.0...v3.25.0)

---
updated-dependencies:
- dependency-name: github.com/osrg/gobgp/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-18 09:47:41 +00:00
Daniel Finneran
979c016d98 Merge pull request #817 from thebsdbox/linting
fixes to linting
2024-04-18 10:45:32 +01:00
Dan Finneran
8e4abea78e fixes to linting
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-04-18 09:35:04 +00:00
Daniel Finneran
3caedde7fe Merge pull request #801 from kube-vip/dependabot/github_actions/anchore/sbom-action-0.15.10
Bump anchore/sbom-action from 0.15.8 to 0.15.10
2024-04-18 10:24:13 +01:00
Daniel Finneran
fb43a48a87 Merge pull request #792 from kube-vip/dependabot/go_modules/k8s.io/apimachinery-0.29.3
Bump k8s.io/apimachinery from 0.29.1 to 0.29.3
2024-04-18 10:23:50 +01:00
Daniel Finneran
12e298bb22 Merge pull request #807 from kube-vip/dependabot/docker/golang-1.22.2-alpine3.18
Bump golang from 1.22.0-alpine3.18 to 1.22.2-alpine3.18
2024-04-18 10:23:32 +01:00
Daniel Finneran
f514269408 Merge pull request #778 from thebsdbox/remove_start
Removal of deprecated start command
2024-04-18 10:22:55 +01:00
Daniel Finneran
2c5d366487 Merge pull request #770 from lubronzhan/topic/lubron/fix-589
Mirror traffic of service interface to a dest interface
2024-04-18 10:22:32 +01:00
Daniel Finneran
fea05fa3ad Merge branch 'main' into topic/lubron/fix-589 2024-04-18 10:10:23 +01:00
Dan Finneran
5c515f0c0d Adds annotation support for service interfaces.
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-04-18 08:56:36 +00:00
Daniel Finneran
b0acf844ca Merge pull request #812 from wyike/fix_masquerade_issues
Fixes on masquerade forwarding mode
2024-04-18 09:54:11 +01:00
Daniel Finneran
bd3df6b616 Merge pull request #811 from d-uzlov/fix-nodename
Use node name for service election and lease holder name instead of hostname
2024-04-17 15:32:16 +01:00
Yike Wang
5100cd0e9a Fixes on masquerade forwarding mode
Signed-off-by: Yike Wang <yikew@vmware.com>
2024-04-17 08:59:44 +08:00
Danil Uzlov
04d863310b improve readability
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-11 03:35:02 +07:00
Danil Uzlov
29713acfeb fix golangci-lint better
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-11 03:32:49 +07:00
Danil Uzlov
8d607b89be fix golangci-lint
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-11 03:27:33 +07:00
Danil Uzlov
877ee75e75 go fmt
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-11 03:20:54 +07:00
Danil Uzlov
d3da4d934a check node names in services test
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-11 02:04:41 +07:00
Debian
c3e9a13f1c add hostname e2e test
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-11 02:04:36 +07:00
Danil Uzlov
59ad5ec9a2 fix e2e tests
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-10 21:28:09 +07:00
Danil Uzlov
8a80a72d94 fix golangci-lint
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-10 18:21:15 +07:00
Danil Uzlov
cb1e63c302 fix unit tests
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-10 17:58:20 +07:00
Danil Uzlov
e87ef6b3b8 go fmt
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-10 17:44:58 +07:00
Danil Uzlov
f87665c67f add fallback for hostname when node name is not available
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-10 17:43:53 +07:00
Daniel Finneran
c9d11e7123 Merge pull request #795 from adavis10006/fix-ddns
fix: add ddns address as a network
2024-04-10 09:59:55 +01:00
Daniel Finneran
51512201ea Merge pull request #809 from starbops/iptables-legacy
fix: add iptables-legacy package
2024-04-10 09:59:25 +01:00
Danil Uzlov
e37091c900 use node name instead of hostname
Signed-off-by: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com>
2024-04-09 18:37:11 +07:00
Zespre Schmidt
b484be799e fix: add iptables-legacy package
Signed-off-by: Zespre Schmidt <starbops@zespre.com>
2024-04-08 15:21:56 +08:00
Shang-Kuei, Chen
2134c6b1fe fix: add ddns address as a network
Signed-off-by: Shang-Kuei, Chen <adavis10006@gmail.com>
2024-04-08 14:37:16 +08:00
dependabot[bot]
fbc6904fa0 Bump golang from 1.22.0-alpine3.18 to 1.22.2-alpine3.18
Bumps golang from 1.22.0-alpine3.18 to 1.22.2-alpine3.18.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 02:41:21 +00:00
Marcel Fest
0cedf6a97d Merge pull request #805 from Bao0ne/offline
fix: Modify the ImagePullPolicy used by cli manifest
2024-04-07 11:50:42 +02:00
wang
932fe09870 fix: Modify the ImagePullPolicy used by cli manifest
Signed-off-by: wang <wanglei@tdology.com>
2024-04-06 17:11:01 +08:00
Daniel Finneran
f1cf044eae Merge pull request #782 from lou-lan/fix/masquerade
Add masquerade mode
2024-04-02 11:06:22 +01:00
dependabot[bot]
a10a478f24 Bump anchore/sbom-action from 0.15.8 to 0.15.10
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.15.8 to 0.15.10.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Commits](https://github.com/anchore/sbom-action/compare/v0.15.8...v0.15.10)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-01 02:11:42 +00:00
lou-lan
d1541c3464 Add masquerade mode
Signed-off-by: lou-lan <loulan@loulan.me>
2024-03-20 19:23:05 +08:00
dependabot[bot]
4e3717f089 Bump k8s.io/apimachinery from 0.29.1 to 0.29.3
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.29.1 to 0.29.3.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.29.1...v0.29.3)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 02:47:29 +00:00
Daniel Finneran
2aa799c9d0 Merge pull request #773 from kube-vip/dependabot/go_modules/go.uber.org/zap-1.27.0
Bump go.uber.org/zap from 1.26.0 to 1.27.0
2024-03-10 16:31:49 +00:00
Daniel Finneran
bb08489898 Merge pull request #783 from lubronzhan/topic/lubron/fix-781
Fix netmask of ipvs to consider ipv6 case
2024-03-10 16:30:49 +00:00
Daniel Finneran
1ea277f43e Merge pull request #777 from thebsdbox/egress_fix
Fixes conntrack deleting wrong connections and cleaning old SNAT rules
2024-03-10 16:23:45 +00:00
Daniel Finneran
2c4ff6949a Update Makefile for next release 2024-03-10 17:23:30 +01:00
lubronzhan
1a4bd01332 Refactor
Signed-off-by: lubronzhan <lubronzhan@gmail.com>
2024-03-10 09:13:16 -07:00
lubronzhan
9e2a13c133 Fix netmask of ipvs to consider ipv6 case
Signed-off-by: lubronzhan <lubronzhan@gmail.com>
2024-03-09 22:38:07 -08:00
Dan Finneran
39fbbc57ac linting fixes
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-03-01 14:14:56 +00:00
Dan Finneran
b9f8c3b0f5 Removal of deprecated start command
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-03-01 12:29:05 +00:00
Daniel Finneran
ac5895a311 Merge pull request #775 from lubronzhan/topic/lubron/change_fatal
Exit(1) if service's leaderelection.onStartedLeading error out instead of silently continue
2024-03-01 11:05:44 +00:00
Daniel Finneran
e7eaa70fd4 Merge pull request #776 from lubronzhan/topic/lubron/fix-771
Fix flaky e2e test
2024-03-01 11:05:23 +00:00
Dan Finneran
76cf59c7a7 Fixes issues around conntrack deleting wrong connections and cleaning up old vip SNAT entries
Signed-off-by: Dan Finneran <daniel.finneran@gmail.com>
2024-03-01 10:54:50 +00:00
lubronzhan
a05ecbccb7 Consider 500 status code as a positive result of reaching control plane VIP
Signed-off-by: lubronzhan <lubronzhan@gmail.com>
2024-02-29 22:25:22 -08:00
lubronzhan
c531414542 Exit 1 if service's leaderelection onStartedLeading error out instead of silently continue
Signed-off-by: lubronzhan <lubronzhan@gmail.com>
2024-02-29 21:58:29 -08:00
lubronzhan
70b4728492 Resolve comments
Signed-off-by: lubronzhan <lubronzhan@gmail.com>
2024-02-29 21:48:20 -08:00
Lubron Zhan
835007b07a Refactor
Signed-off-by: Lubron Zhan <lubronzhan@gmail.com>
2024-02-29 21:48:20 -08:00
Lubron Zhan
6d0c132519 Try clean up before deleting
Signed-off-by: Lubron Zhan <lubronzhan@gmail.com>
2024-02-29 21:48:20 -08:00
Lubron Zhan
485cf6a0dd Fix the variable
Signed-off-by: Lubron Zhan <lubronzhan@gmail.com>
2024-02-29 21:48:20 -08:00
Lubron Zhan
1338e4fcd3 Move mirroring traffic code to servicesWatcher
Signed-off-by: Lubron Zhan <lubronzhan@gmail.com>
2024-02-29 21:48:20 -08:00
lubronzhan
981355d910 Mirror traffic of service interface to a dest interface
Signed-off-by: lubronzhan <lubronzhan@gmail.com>
2024-02-29 21:48:20 -08:00
dependabot[bot]
e89d6f9f00 Bump go.uber.org/zap from 1.26.0 to 1.27.0
Bumps [go.uber.org/zap](https://github.com/uber-go/zap) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/uber-go/zap/releases)
- [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/zap/compare/v1.26.0...v1.27.0)

---
updated-dependencies:
- dependency-name: go.uber.org/zap
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-26 02:56:34 +00:00
124 changed files with 12049 additions and 5425 deletions

View File

@@ -26,6 +26,6 @@ jobs:
with:
ref: ${{ github.ref_name }}
- name: Anchore SBOM Action
uses: anchore/sbom-action@v0.15.8
uses: anchore/sbom-action@v0.20.4
with:
format: cyclonedx-json

51
.github/workflows/ci-pull-request.yaml vendored Normal file
View File

@@ -0,0 +1,51 @@
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
max-parallel: 1
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
- name: Install Go
uses: actions/setup-go@v5
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: make e2e-tests129-arp
if: matrix.mode== 'arp'
- name: Run RT mode tests v1.29.0 onwards
run: 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 make e2e-tests129-bgp
if: matrix.mode== 'bgp'
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

View File

@@ -1,18 +1,16 @@
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
- name: Install Go
@@ -37,46 +35,14 @@ 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
- name: Install Go
uses: actions/setup-go@v5
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
@@ -88,10 +54,9 @@ jobs:
- 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"

View File

@@ -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

View File

@@ -27,13 +27,20 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Metadata
uses: docker/metadata-action@v5.7.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,

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:experimental
FROM golang:1.22.0-alpine3.18 as dev
FROM golang:1.24.5-alpine3.22 as dev
RUN apk add --no-cache git ca-certificates make
RUN adduser -D appuser
COPY . /src/

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:experimental
FROM golang:1.22.0-alpine3.18 as dev
FROM golang:1.24.5-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.1
# 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 /

View File

@@ -5,7 +5,7 @@ TARGET := kube-vip
.DEFAULT_GOAL := $(TARGET)
# These will be provided to the target
VERSION := v0.7.1
VERSION := v1.0.0
BUILD := `git rev-parse HEAD`
@@ -17,7 +17,7 @@ LDFLAGS=-ldflags "-s -w -X=main.Version=$(VERSION) -X=main.Build=$(BUILD) -extld
DOCKERTAG ?= $(VERSION)
REPOSITORY ?= 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 ./...
@@ -113,9 +115,17 @@ manifests:
@./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 daemonset --interface eth0 --vip 192.168.0.1 --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 --arp --leaderElection --controlplane --services
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --enableLoadBalancer
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --bgp --controlplane --services
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --inCluster
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --inCluster --enableLoadBalancer
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --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 --arp --servicesElection --interface eth0 | kubectl apply -f -
kind-reload:
kind load docker-image $(REPOSITORY)/$(TARGET):$(DOCKERTAG) --name kube-vip
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

View File

@@ -4,7 +4,7 @@ High Availability and Load-Balancing
![](https://github.com/kube-vip/kube-vip/raw/main/kube-vip.png)
[![Build and publish main image regularly](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml/badge.svg)](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml)
[![Build and publish main image regularly](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml/badge.svg)](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml) [![LFX Health Score](https://img.shields.io/static/v1?label=Health%20Score&message=Healthy&color=A7F3D0&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/kube-vip) [![LFX Active Contributors](https://img.shields.io/static/v1?label=Active%20contributors%20(1Y)&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?

View File

@@ -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,25 +33,36 @@ 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
}
// Ensure there is an address to generate the CIDR from
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
if err != nil {
log.Error("generating VIPSubnet", "err", err)
return
}
}
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
@@ -61,30 +73,41 @@ 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
}
// Ensure there is an address to generate the CIDR from
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
if err != nil {
log.Error("generating VIPSubnet", "err", err)
return
}
}
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)

View File

@@ -3,23 +3,25 @@ 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")
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 +31,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,19 +40,30 @@ 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
}
// Ensure there is an address to generate the CIDR from
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
if err != nil {
log.Error("config parse", "err", err)
return
}
}
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
@@ -61,21 +74,29 @@ 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
}
// Ensure there is an address to generate the CIDR from
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
if err != nil {
log.Error("config parse", "err", err)
return
}
}
cfg := kubevip.GenerateDaemonsetManifestFromConfig(&initConfig, Release.Version, inCluster, taint)
@@ -86,22 +107,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)
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))
},
}

View File

@@ -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)
}
}
}
},
}

View File

@@ -3,46 +3,33 @@ package cmd
import (
"context"
"fmt"
"net"
"net/http"
"os"
"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/vip"
)
// Path to the configuration file
var configPath 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 +43,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 +53,30 @@ 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")
// 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 +93,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")
@@ -139,12 +122,15 @@ func init() {
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.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().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,7 +142,6 @@ 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")
kubeVipCmd.AddCommand(kubeKubeadm)
@@ -164,8 +149,10 @@ func init() {
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 +166,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 +176,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 +184,26 @@ 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)
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 +212,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)
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 +240,33 @@ var kubeVipService = &cobra.Command{
var kubeVipManager = &cobra.Command{
Use: "manager",
Short: "Start the kube-vip manager",
Run: func(cmd *cobra.Command, args []string) {
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)
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)
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 +294,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 +319,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 +376,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 +388,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 +404,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 +422,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 +439,35 @@ 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) (string, error) {
var cidrs []string
addresses := strings.Split(address, ",")
for _, a := range addresses {
ip := net.ParseIP(a)
if ip == nil {
ips, err := net.LookupIP(a)
if len(ips) == 0 || err != nil {
return "", fmt.Errorf("invalid IP address: %s from [%s], %v", a, address, err)
}
ip = ips[0]
}
if ip.To4() != nil {
cidrs = append(cidrs, "32")
} else {
cidrs = append(cidrs, "128")
}
}
return strings.Join(cidrs, ","), nil
}

View File

@@ -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
View 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

View 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
View 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

227
go.mod
View File

@@ -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.3.3+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/nftables v0.3.0
github.com/gookit/slog v0.5.8
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.23.4
github.com/onsi/gomega v1.37.0
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.22.0
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
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/vishvananda/netlink v1.3.1
go.etcd.io/etcd/api/v3 v3.6.1
go.etcd.io/etcd/client/pkg/v3 v3.6.1
go.etcd.io/etcd/client/v3 v3.6.1
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
k8s.io/api v0.33.3
k8s.io/apimachinery v0.33.3
k8s.io/client-go v0.33.3
k8s.io/klog/v2 v2.130.1
sigs.k8s.io/kind v0.29.0
sigs.k8s.io/yaml v1.4.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/Microsoft/go-winio v0.4.14 // 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/containerd/log v0.1.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.1 // 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.7.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.6.9 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/gookit/goutil v0.6.18 // indirect
github.com/gookit/gsr v0.1.0 // 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/atomicwriter v0.1.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/morikuni/aec v1.0.0 // 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.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.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.6 // 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.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // 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-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241210054802-24370beab758 // 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/v4 v4.6.0 // indirect
)

913
go.sum

File diff suppressed because it is too large Load Diff

149
pkg/arp/arp.go Normal file
View File

@@ -0,0 +1,149 @@
package arp
import (
"context"
log "log/slog"
"sync"
"time"
"github.com/kube-vip/kube-vip/pkg/kubevip"
"github.com/kube-vip/kube-vip/pkg/vip"
)
type Manager struct {
instances map[string]*Instance
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{
instances: make(map[string]*Instance),
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, ok := m.instances[instance.Name()]
if !ok {
log.Info("inserting ARP/NDP instance", "name", instance.Name())
m.instances[instance.Name()] = instance
} else {
i.mu.Lock()
defer i.mu.Unlock()
i.counter++
}
}
func (m *Manager) Remove(instance *Instance) {
if i, ok := m.instances[instance.Name()]; ok {
i.mu.Lock()
defer i.mu.Unlock()
if i.counter > 1 {
i.counter--
} else {
log.Info("removing ARP/NDP instance", "name", instance.Name())
delete(m.instances, instance.Name())
}
}
}
func (m *Manager) Count(name string) int {
if i, ok := m.instances[name]; ok {
i.mu.Lock()
defer i.mu.Unlock()
return i.counter
}
return 0
}
func (m *Manager) StartAdvertisement(ctx context.Context) {
log.Info("Starting ARP/NDP advertisement")
for {
select {
case <-ctx.Done(): // if cancel() execute
return
default:
for _, instance := range m.instances {
if instance.counter > 0 {
ensureIPAndSendGratuitous(instance)
}
}
}
if m.config.ArpBroadcastRate < 500 {
log.Error("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)
}
}
// 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", "IP", ipString, "interface", iface)
// if _, err := instance.network.AddIP(false); err != nil {
// log.Error("failed to recreate address", "IP", ipString, "interface", iface)
// }
}
}
// Ensure the address exists on the interface before attempting to ARP
// if instance.network.HasEndpoints() {
if added, err := instance.network.AddIP(true); err != nil {
log.Warn(err.Error())
} else if added {
log.Warn("Re-applied the VIP configuration", "ip", ipString, "interface", iface)
}
// }
if vip.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())
}
}
}

88
pkg/backend/backend.go Normal file
View File

@@ -0,0 +1,88 @@
package backend
import (
"fmt"
"time"
log "log/slog"
"github.com/kube-vip/kube-vip/pkg/k8s"
"github.com/kube-vip/kube-vip/pkg/utils"
"github.com/kube-vip/kube-vip/pkg/vip"
"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 vip.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))
}
}
}

View File

@@ -7,9 +7,11 @@ import (
"strconv"
"strings"
"github.com/golang/protobuf/ptypes" //nolint
"github.com/golang/protobuf/ptypes/any"
//nolint
"github.com/kube-vip/kube-vip/pkg/vip"
api "github.com/osrg/gobgp/v3/api"
"github.com/osrg/gobgp/v3/pkg/server"
"google.golang.org/protobuf/types/known/anypb"
)
// AddPeer will add peers to the BGP configuration
@@ -42,36 +44,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 vip.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 +141,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,16 +156,16 @@ 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
@@ -115,11 +175,13 @@ func (b *Server) getPath(ip net.IP) (path *api.Path) {
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")
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
}
@@ -162,14 +224,118 @@ func ParseBGPPeerConfig(config string) (bgpPeers []Peer, err error) {
}
}
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 := Peer{
Address: address,
AS: uint32(ASNumber),
Password: password,
MultiHop: multiHop,
Address: address,
AS: uint32(ASNumber),
Port: uint16(port),
Password: password,
MultiHop: multiHop,
MpbgpNexthop: mpbgpNexthop,
MpbgpIPv4: mpbgpIPv4,
MpbgpIPv6: mpbgpIPv6,
}
bgpPeers = append(bgpPeers, peerConfig)
}
return
}
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"
}
setName := fmt.Sprintf("peer-%s", p.Conf.NeighborAddress)
policyName := fmt.Sprintf("%s-%s", setName, familyType)
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,
},
},
},
}
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
}

54
pkg/bgp/peers_test.go Normal file
View File

@@ -0,0 +1,54 @@
package bgp
import (
"reflect"
"testing"
)
func TestParseBGPPeerConfig(t *testing.T) {
type args struct {
config string
}
tests := []struct {
name string
args args
wantBgpPeers []Peer
wantErr bool
}{
{
name: "IPv4, default port",
args: args{config: "192.168.0.10:65000::false,192.168.0.11:65000::false"},
wantBgpPeers: []Peer{
{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: []Peer{
{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: []Peer{
{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 := 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)
}
})
}
}

View File

@@ -5,35 +5,49 @@ import (
"fmt"
"time"
log "log/slog"
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"
)
// 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 *Config) (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,
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 +56,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 +65,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
}

View File

@@ -1,30 +1,145 @@
package bgp
import gobgp "github.com/osrg/gobgp/v3/pkg/server"
import (
"fmt"
"net"
"github.com/kube-vip/kube-vip/pkg/vip"
api "github.com/osrg/gobgp/v3/api"
gobgp "github.com/osrg/gobgp/v3/pkg/server"
"github.com/prometheus/client_golang/prometheus"
"github.com/vishvananda/netlink"
)
// Peer defines a BGP Peer
type Peer struct {
Address string
AS uint32
Password string
MultiHop bool
Address string
Port uint16
AS uint32
Password string
MultiHop bool
MpbgpNexthop string
MpbgpIPv4 string
MpbgpIPv6 string
}
func (p *Peer) setMpbgpOptions(server *Config) {
if p.MpbgpNexthop == "" {
p.MpbgpNexthop = server.MpbgpNexthop
}
if p.MpbgpIPv4 == "" {
p.MpbgpIPv4 = server.MpbgpIPv4
}
if p.MpbgpIPv6 == "" {
p.MpbgpIPv6 = server.MpbgpIPv6
}
}
func (p *Peer) findMpbgpAddresses(ap *api.Peer, server *Config) (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 := vip.GetInterfaceByIP(server.SourceIP)
if err != nil {
return "", "", fmt.Errorf("failed to get interface by IP: %v", err)
}
if vip.IsIPv4(server.SourceIP) {
// Get the non link-local IPv6 address on that interface
ipv6Address, err = vip.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 = vip.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 = vip.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 = vip.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
}
// Config defines the BGP server configuration
type Config struct {
AS uint32
RouterID string
SourceIP string
SourceIF string
AS uint32
RouterID string
SourceIP string
SourceIF string
MpbgpNexthop string
MpbgpIPv4 string
MpbgpIPv6 string
HoldTime uint64
KeepaliveInterval uint64
Peers []Peer
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
}
// Server manages a server object
type Server struct {
s *gobgp.BgpServer
c *Config
// This is a prometheus gauge indicating the state of the sessions.
// 1 means "ESTABLISHED", 0 means "NOT ESTABLISHED"
BGPSessionInfoGauge *prometheus.GaugeVec
}

View File

@@ -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
}

View File

@@ -11,15 +11,13 @@ 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/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 +32,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 +63,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 +132,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 +148,27 @@ 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
// 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 +184,26 @@ 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()
deleted, err := cluster.Network[i].DeleteIP()
if err != nil {
log.Warnf("%v", err)
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)
},
}
@@ -290,17 +284,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 +322,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 +346,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 +362,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
}

View File

@@ -2,26 +2,32 @@ 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/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 +39,268 @@ 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() {
var arpWG sync.WaitGroup
for i := range cluster.Network {
network := cluster.Network[i]
if err := network.SetMask(c.VIPSubnet); err != nil {
log.Error("failed to set mask", "subnet", c.VIPSubnet, "err", err)
panic("")
}
if network.IsDDNS() {
if err := cluster.StartDDNS(ctxDNS); err != nil {
log.Error(err)
log.Error(err.Error())
}
}
// 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 {
if _, err = network.AddIP(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
}
backendMap := &backendMapV4
if isNetworkV6 {
backendMap = &backendMapV6
}
for entry := range *backendMap {
if entry.Check() {
_, err = network.AddIP(true)
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
for i := range cluster.Network {
network := cluster.Network[i]
err := network.DeleteIP()
if err != nil {
log.Warnf("Attempted to clean existing VIP => %v", err)
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)
}
if c.EnableRoutingTable && (c.EnableLeaderElection || c.EnableServicesElection) {
err = network.AddRoute()
err = network.AddRoute(false)
if err != nil {
log.Warnf("%v", err)
log.Warn(err.Error())
}
} else if !c.EnableRoutingTable {
err = network.AddIP()
if err != nil {
log.Warnf("%v", err)
if _, err = network.AddIP(false); err != nil {
log.Warn(err.Error())
}
}
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 +310,36 @@ 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()) > 0 {
continue
}
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 +347,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 vip.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.Remove(arpInstance)
}

View File

@@ -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,17 @@ 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()
_, err = cluster.Network[i].AddIP(false)
if err != nil {
log.Warnf("%v", err)
log.Warn(err.Error())
}
}
@@ -41,7 +43,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 +53,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 +80,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)
}

99
pkg/egress/egress.go Normal file
View File

@@ -0,0 +1,99 @@
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/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 vip.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
View 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
}
}
}

View File

@@ -0,0 +1,125 @@
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])
*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) {
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)
}
}
}
}

View 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
}

View File

@@ -0,0 +1,178 @@
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.FindServiceInstance(service, *rt.instances)
if instance == nil {
log.Error("failed to find the instance", "service", 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 instance := instance.FindServiceInstance(service, *instances); instance != nil {
for _, cluster := range instance.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
}

View 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 ""
}

View 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 {
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)
}

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
@@ -221,6 +222,6 @@ 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)
log.Error("Failed to resign after the context was canceled", "err", err)
}
}

View File

@@ -91,12 +91,12 @@ func TestRunElectionWithTwoMembersAndReelection(t *testing.T) {
config1 := configBase
config1.EtcdConfig.Client = cliMember1
config1.MemberID = "my-host"
uniqueID := rand.Int63()
uniqueID := rand.Uint64()
config1.MemberUniqueID = &uniqueID
config1.Callbacks = baseCallbacksForName(config1.MemberID)
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")
log.Println("Losing the leadership on purpose by stopping renewing the lease")
g.Expect(cliMember1.Lease.Close()).To(Succeed())
log.Println("Member1 leases closed")
}

482
pkg/instance/instance.go Normal file
View File

@@ -0,0 +1,482 @@
package instance
import (
"fmt"
"net"
"strconv"
"strings"
"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/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)
//instanceUID := string(svc.UID)
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 vip.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,
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
}
// FetchIngressAddresses tries to get the addresses from status.loadBalancerIP
func FetchLoadBalancerIngressAddresses(s *v1.Service) []string {
// If the service has no status, return empty
lbStatusAddresses := []string{}
if len(s.Status.LoadBalancer.Ingress) == 0 {
return lbStatusAddresses
}
for _, ingress := range s.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
lbStatusAddresses = append(lbStatusAddresses, ingress.IP)
}
// TODO: Handle hostname if needed
}
return lbStatusAddresses
}
// 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[kubevip.LoadbalancerIPAnnotation]; annotationAvailable {
ips := strings.Split(v, ",")
var trimmedIPs []string
for _, ip := range ips {
trimmedIPs = append(trimmedIPs, strings.TrimSpace(ip))
}
return trimmedIPs
}
}
lbStatusAddresses := []string{}
if !annotationAvailable {
if len(s.Status.LoadBalancer.Ingress) > 0 {
for _, ingress := range s.Status.LoadBalancer.Ingress {
lbStatusAddresses = append(lbStatusAddresses, ingress.IP)
}
}
}
lbIP := net.ParseIP(s.Spec.LoadBalancerIP)
isLbIPv4 := vip.IsIPv4(s.Spec.LoadBalancerIP)
if len(lbStatusAddresses) > 0 {
for _, a := range lbStatusAddresses {
if lbStatusIP := net.ParseIP(a); lbStatusIP != nil && lbIP != nil && vip.IsIPv4(a) == isLbIPv4 && !lbIP.Equal(lbStatusIP) {
return []string{s.Spec.LoadBalancerIP}
}
}
return lbStatusAddresses
}
if s.Spec.LoadBalancerIP != "" {
return []string{s.Spec.LoadBalancerIP}
}
return []string{}
}
func FindServiceInstance(svc *v1.Service, instances []*Instance) *Instance {
log.Debug("finding service", "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]
}
}
return nil
}

View File

@@ -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
View 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)
}

View File

@@ -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)

View File

@@ -0,0 +1,49 @@
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"
LoadbalancerIPAnnotation = "kube-vip.io/loadbalancerIPs"
LoadbalancerHostname = "kube-vip.io/loadbalancerHostname"
ServiceInterface = "kube-vip.io/serviceInterface"
UpnpEnabled = "kube-vip.io/forwardUPNP"
RPFilter = "kube-vip.io/rp_filter" // Set the return path filter for a specific service interface
)

View File

@@ -2,8 +2,12 @@ package kubevip
import (
"encoding/json"
"fmt"
"math"
"math/bits"
"os"
"strconv"
"strings"
"github.com/kube-vip/kube-vip/pkg/bgp"
"github.com/kube-vip/kube-vip/pkg/detector"
@@ -16,17 +20,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 +35,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 +103,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 +127,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 +170,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 +205,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 +233,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 != "" {
@@ -296,11 +313,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
@@ -405,6 +429,24 @@ func ParseEnvironment(c *Config) error {
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 +501,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 +542,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,13 +607,64 @@ 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
}
return nil

View File

@@ -33,12 +33,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 +74,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 +110,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 +158,9 @@ const (
// cpDetect will attempt to automatically find a working address for the control plane from loopback
cpDetect = "cp_detect"
// kubernetesAddris the address of the Kubernetes API server on this machine
kubernetesAddr = "kubernetes_addr"
// svcEnable enables the Kubernetes service feature
svcEnable = "svc_enable"
@@ -169,6 +179,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 +203,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 +212,26 @@ 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"
)

View File

@@ -2,11 +2,14 @@ package kubevip
import (
"fmt"
"log"
"strconv"
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"
@@ -14,11 +17,43 @@ import (
"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,
},
},
}
@@ -121,6 +188,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 +206,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 +228,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 +386,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 +407,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 +417,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 +513,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 +539,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",
@@ -519,15 +593,8 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
{
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",
},
},
},
ImagePullPolicy: corev1.PullIfNotPresent,
SecurityContext: securityContext,
Args: []string{
command,
},
@@ -566,26 +633,6 @@ 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
}
@@ -673,6 +720,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)
}

View File

@@ -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 {

View File

@@ -7,7 +7,7 @@ import (
// 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 +27,9 @@ type Config struct {
// DetectControlPlane, will attempt to find the control plane from loopback (127.0.0.1)
DetectControlPlane bool `yaml:"detectControlPlane"`
// KubernetesAddris 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,6 +45,9 @@ 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"`
@@ -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"`
@@ -126,21 +132,6 @@ type Config struct {
BGPPeerConfig bgp.Peer
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,31 @@ 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"`
}
// KubernetesLeaderElection defines all of the settings for Kubernetes KubernetesLeaderElection
@@ -188,7 +202,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 +222,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 +231,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"`
}

View File

@@ -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/vip"
"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 vip.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
}

View File

@@ -26,6 +26,14 @@ func Test_ipAndFamily(t *testing.T) {
want: netip.AddrFrom4([4]byte{192, 168, 0, 20}),
want1: ipvs.INET,
},
{
name: "IPv6",
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) {

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -1,43 +1,45 @@
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/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 +48,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 +60,100 @@ 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
}
// 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 +171,37 @@ 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)
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)
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 +214,17 @@ 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,
}, 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
@@ -154,6 +238,26 @@ func (sm *Manager) Start() error {
// 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)
}
}()
}
// If BGP is enabled then we start a server instance that will broadcast VIPs
if sm.config.EnableBGP {
@@ -163,27 +267,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 +324,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 +334,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
}

View File

@@ -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,6 +27,9 @@ 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
@@ -43,7 +44,7 @@ func (sm *Manager) startARP() error {
}()
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 +57,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 +76,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 +125,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 +150,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)
},
},
})

View File

@@ -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
@@ -98,7 +73,7 @@ func (sm *Manager) startBGP() error {
}()
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 +87,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 +99,18 @@ 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)
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
}

View File

@@ -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,146 @@ 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")
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()
}()
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.EnableControlPlane {
cpCluster, err = cluster.InitCluster(sm.config, false, sm.intfMgr, sm.arpMgr)
if err != nil {
return err
return fmt.Errorf("cluster initialization error: %w", err)
}
} 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,
},
clusterManager, err := initClusterManager(sm)
if err != nil {
return fmt.Errorf("cluster manager initialization error: %w", err)
}
// 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)
}
},
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()
}
}
log.Fatal("lost leadership, restarting kube-vip")
},
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)
},
},
})
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.Infof("beginning watching services without leader election")
err = sm.servicesWatcher(ctx, sm.syncServices)
ns, err := returnNameSpace()
if err != nil {
log.Errorf("Cannot watch services, %v", err)
log.Warn("unable to auto-detect namespace", "dropping to", 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.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,
},
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.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)
},
},
})
} 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)
}
}
}
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 +166,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
}

View File

@@ -2,33 +2,26 @@ package manager
import (
"context"
"os"
"strconv"
"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
@@ -55,36 +48,21 @@ func (sm *Manager) startWireguard() 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", 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 +91,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 +113,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)
},
},
})

View File

@@ -5,7 +5,8 @@ import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
log "log/slog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
@@ -28,24 +29,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)
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 +59,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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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{}
}

View File

@@ -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
}

View File

@@ -4,13 +4,13 @@ import (
"context"
"encoding/base64"
"fmt"
"os"
"regexp"
"strconv"
"strings"
log "log/slog"
"github.com/kube-vip/kube-vip/pkg/bgp"
log "github.com/sirupsen/logrus"
"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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View 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
}

458
pkg/nftables/nftables.go Normal file
View File

@@ -0,0 +1,458 @@
package nftables
import (
"errors"
"fmt"
"log/slog"
"net"
"strconv"
"strings"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
"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, "IPv6", IPv6)
// These don't return errors, so not 100% sure how to guarantee things were created
conn.AddChain(GetSNatChain(IPv6, service))
conn.Flush()
// 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
}

View 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
}

36
pkg/services/context.go Normal file
View File

@@ -0,0 +1,36 @@
package services
import (
"context"
"sync"
)
type Context struct {
Ctx context.Context
Cancel context.CancelFunc
IsActive bool
IsWatched bool
ConfiguredNetworks sync.Map
}
func NewContext(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
}

360
pkg/services/egress.go Normal file
View File

@@ -0,0 +1,360 @@
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/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 := vip.IsIPv6(ip)
matchingFamily := []string{}
for _, cidr := range cidrs {
// Is the ip an IPv6 address
if isV6 {
if vip.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 vip.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 !vip.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 !vip.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 vip.IsIPv4(podIP) != vip.IsIPv4CIDR(podCidr) {
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "podCIDR", podCidr)
return nil
}
if vip.IsIPv4(vipIP) != vip.IsIPv4CIDR(serviceCidr) {
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "serviceCIDR", serviceCidr)
return nil
}
if vip.IsIPv4(vipIP) != vip.IsIPv4(podIP) {
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "vipIP", vipIP)
return nil
}
protocol := iptables.ProtocolIPv4
if vip.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, vip.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 !vip.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
}

View File

@@ -1,4 +1,4 @@
package manager
package services
import "testing"

127
pkg/services/leader.go Normal file
View File

@@ -0,0 +1,127 @@
package services
import (
"context"
"fmt"
"sync"
"time"
log "log/slog"
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"
)
var (
svcLocks map[string]*sync.Mutex
)
func init() {
svcLocks = make(map[string]*sync.Mutex)
}
// 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
}
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 := fmt.Sprintf("kubevip-%s", service.Name)
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,
},
}
childCtx, childCancel := context.WithCancel(ctx)
defer childCancel()
if _, ok := svcLocks[serviceLease]; !ok {
svcLocks[serviceLease] = new(sync.Mutex)
}
svcLocks[serviceLease].Lock()
defer svcLocks[serviceLease].Unlock()
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
// start the leader election code loop
leaderelection.RunOrDie(childCtx, 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)
childCancel()
}
},
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
}

344
pkg/services/processor.go Normal file
View File

@@ -0,0 +1,344 @@
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/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
}
func NewServicesProcessor(config *kubevip.Config, bgpServer *bgp.Server,
clientSet *kubernetes.Clientset, rwClientSet *kubernetes.Clientset, shutdownChan chan struct{},
intfMgr *networkinterface.Manager, arpMgr *arp.Manager) *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,
}
}
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["kube-vip.io/ignore"] == "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 := instance.FetchServiceAddresses(svc)
// We only care about LoadBalancer services that have been allocated an address
if len(svcAddresses) <= 0 {
return true, nil
}
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)
var originalService []string
shouldGarbageCollect := true
if i != nil {
originalService = instance.FetchServiceAddresses(i.ServiceSnapshot)
shouldGarbageCollect = !reflect.DeepEqual(originalService, svcAddresses)
}
if shouldGarbageCollect {
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(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 {
if i != nil {
originalService := instance.FetchServiceAddresses(i.ServiceSnapshot)
newService := instance.FetchServiceAddresses(svc)
if !reflect.DeepEqual(originalService, newService) {
// Calls the cancel function of the context
if svcCtx != nil {
log.Warn("(svcs) The load balancer has changed, cancelling original load balancer")
svcCtx.Cancel()
log.Warn("(svcs) waiting for load balancer to finish")
<-svcCtx.Ctx.Done()
}
err = p.deleteService(svc.UID)
if err != nil {
log.Error("(svc) unable to remove", "service", svc.UID)
}
p.svcMap.Delete(svc.UID)
}
// in theory this should never fail
}
}
}
// 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 {
log.Debug("(svcs) has been added/modified with addresses", "service name", svc.Name, "ip", instance.FetchServiceAddresses(svc))
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("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 && svcCtx.IsActive {
// We only care about LoadBalancer services
if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
return true, nil
}
// We can ignore this service
if svc.Annotations["kube-vip.io/ignore"] == "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.Cancel()
log.Warn("(svcs) waiting for load balancer to finish")
<-svcCtx.Ctx.Done()
p.svcMap.Delete(svc.UID)
}
if p.config.EnableLeaderElection && !p.config.EnableServicesElection {
if p.config.EnableBGP {
endpoints.ClearBGPHosts(svc, &p.ServiceInstances, p.bgpServer)
} else if p.config.EnableRoutingTable {
endpoints.ClearRoutes(svc, &p.ServiceInstances)
}
}
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)
}

516
pkg/services/services.go Normal file
View File

@@ -0,0 +1,516 @@
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"
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/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:
log.Debug("[service] delete", "namespace", svc.Namespace, "name", svc.Name)
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)
if err := p.addService(ctx, svc); err != nil {
return fmt.Errorf("error adding service %s/%s: %w", svc.Namespace, svc.Name, err)
}
case ActionNone:
log.Debug("[service] no action", "namespace", svc.Namespace, "name", svc.Name)
}
log.Debug("[FINISHED] Service Sync", "namespace", svc.Namespace, "name", svc.Name)
return nil
}
func (p *Processor) getServiceInstanceAction(svc *v1.Service) ServiceInstanceAction {
// protect against multiple calls
addresses := instance.FetchServiceAddresses(svc)
ingressIPs := instance.FetchLoadBalancerIngressAddresses(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" {
return ActionDelete
}
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(ingressIPs, instance.DHCPInterfaceIP) {
return ActionDelete
}
}
if !instance.IsDHCP {
if address == "0.0.0.0" {
return ActionDelete
}
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(ingressIPs, address) {
return ActionDelete
}
}
if !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 {
log.Debug("No matching service instance found", "service", svc.Name, "namespace", svc.Namespace, "addresses", addresses)
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.Error("[service] configuring 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.Error("[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 && vip.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.Error("[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 && vip.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.Error("[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.Error("[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)
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 {
for _, vip := range instance.FetchServiceAddresses(updatedInstances[x].ServiceSnapshot) { //updatedInstances[x].ServiceSnapshot.Spec.LoadBalancerIP {
vipSet[vip] = nil
}
}
for _, vip := range instance.FetchServiceAddresses(serviceInstance.ServiceSnapshot) {
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)
}
}
for i := range serviceInstance.VIPConfigs {
if serviceInstance.VIPConfigs[i].EnableBGP {
endpoints.ClearBGPHostsByInstance(serviceInstance, p.bgpServer)
}
}
// 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, "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)
for _, vip := range instance.FetchServiceAddresses(s.ServiceSnapshot) {
for _, port := range s.ServiceSnapshot.Spec.Ports {
for _, gw := range gateways {
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name, "gateway", gw.WANIPv6FirewallControlClient.Location)
forwardSucessful := false
if gw.WANIPv6FirewallControlClient != nil {
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 {
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 {
log.Info("[UPNP] Service should be accessible externally", "port", port.Port)
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, add an annotation to the service
if p.config.EnableARP {
// 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 !vip.IsIP(c.VIP) {
ips, err := vip.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 {
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)
}
}
}
}

View 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
}

View 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
View 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
}

View 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
View 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
}
}

27
pkg/utils/utils.go Normal file
View File

@@ -0,0 +1,27 @@
package utils
import (
"fmt"
"net"
"os"
)
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// 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
}

View File

@@ -2,37 +2,44 @@ package vip
import (
"fmt"
"math"
"net"
"os"
"slices"
"strconv"
"strings"
"sync"
log "log/slog"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
v1 "k8s.io/api/core/v1"
"github.com/kube-vip/kube-vip/pkg/iptables"
"github.com/kube-vip/kube-vip/pkg/networkinterface"
"github.com/kube-vip/kube-vip/pkg/utils"
)
const (
defaultValidLft = 60
iptablesComment = "%s kube-vip load balancer IP"
iptablesCommentMarkRule = "kube-vip load balancer IP set mark for masquerade"
ignoreServiceSecurityAnnotation = "kube-vip.io/ignore-service-security"
)
// Network is an interface that enable managing operations for a given IP
type Network interface {
AddIP() error
AddRoute() error
DeleteIP() error
AddIP(precheck bool) (bool, error)
AddRoute(precheck bool) error
DeleteIP() (bool, error)
DeleteRoute() error
UpdateRoutes() (bool, error)
IsSet() (bool, error)
IP() string
CIDR() string
IPisLinkLocal() bool
PrepareRoute() *netlink.Route
SetIP(ip string) error
SetServicePorts(service *v1.Service)
@@ -42,6 +49,10 @@ type Network interface {
IsDDNS() bool
DDNSHostName() string
DNSName() string
SetMask(mask string) error
SetHasEndpoints(value bool)
HasEndpoints() bool
ARPName() string
}
// network - This allows network configuration
@@ -49,60 +60,77 @@ type network struct {
mu sync.Mutex
address *netlink.Addr
link netlink.Link
link *networkinterface.Link
ports []v1.ServicePort
serviceName string
enableSecurity bool
ignoreSecurity bool
dnsName string
isDDNS bool
forwardMethod string
iptablesBackend string
routeTable int
routingTableType int
routingProtocol int
}
func netlinkParse(addr string) (*netlink.Addr, error) {
mask, err := GetFullMask(addr)
if err != nil {
return nil, err
}
return netlink.ParseAddr(addr + mask)
ipvsEnabled bool
hasEndpoints bool
}
// NewConfig will attempt to provide an interface to the kernel network configuration
func NewConfig(address string, iface string, subnet string, isDDNS bool, tableID int, tableType int, routingProtocol int, dnsMode string) ([]Network, error) {
func NewConfig(address string, iface string, loGlobalScope bool, subnet string, isDDNS bool, tableID int, tableType int,
routingProtocol int, dnsMode, forwardMethod, iptablesBackend string,
ipvsEnabled, enableSecurity bool, intfMgr *networkinterface.Manager) ([]Network, error) {
networks := []Network{}
if IsIP(address) {
result := &network{}
link, err := netlink.LinkByName(iface)
if err != nil {
return networks, errors.Wrapf(err, "could not get link for interface '%s'", iface)
}
link, err := netlink.LinkByName(iface)
if err != nil {
return networks, errors.Wrapf(err, "could not get link for interface '%s'", iface)
networkLink := intfMgr.Get(link)
if IsIP(address) {
result := &network{
link: networkLink,
routeTable: tableID,
routingTableType: tableType,
routingProtocol: routingProtocol,
forwardMethod: forwardMethod,
iptablesBackend: iptablesBackend,
ipvsEnabled: ipvsEnabled,
}
result.link = link
result.routeTable = tableID
result.routingTableType = tableType
result.routingProtocol = routingProtocol
subnet, err = SelectSubnet(address, subnet)
if err != nil {
return networks, fmt.Errorf("unable to select subnet for IP %q from %q: %w", address, subnet, err)
}
// Check if the subnet needs overriding
if subnet != "" {
result.address, err = netlink.ParseAddr(address + subnet)
if err != nil {
return networks, errors.Wrapf(err, "could not parse address '%s'", address)
}
} else {
result.address, err = netlinkParse(address)
if err != nil {
return networks, errors.Wrapf(err, "could not parse address '%s'", address)
}
cidr, err := utils.FormatIPWithSubnetMask(address, subnet)
if err != nil {
return networks, errors.Wrapf(err, "could not format address '%s' with subnetMask '%s'", address, subnet)
}
// Ensure we don't have a global address on loopback
if iface == "lo" {
result.address, err = netlink.ParseAddr(cidr)
if err != nil {
return networks, errors.Wrapf(err, "could not parse address '%s'", address)
}
// set address as deprecated so it isn't used as source address according to RFC 3484
result.address.PreferedLft = 0
// Also set ValidLft so the netlink library actually sets them
result.address.ValidLft = math.MaxInt
if iface == "lo" && !loGlobalScope {
// set host scope on loopback, otherwise global scope will be used by default
result.address.Scope = unix.RT_SCOPE_HOST
}
networks = append(networks, result)
} else {
// try to resolve the address
@@ -111,35 +139,50 @@ func NewConfig(address string, iface string, subnet string, isDDNS bool, tableID
// return early for ddns if no IP is allocated for the domain
// when leader starts, should do get IP from DHCP for the domain
if isDDNS {
result := &network{
link: networkLink,
routeTable: tableID,
routingTableType: tableType,
routingProtocol: routingProtocol,
forwardMethod: forwardMethod,
iptablesBackend: iptablesBackend,
isDDNS: isDDNS,
dnsName: address,
ipvsEnabled: ipvsEnabled,
enableSecurity: enableSecurity,
}
networks = append(networks, result)
return networks, nil
}
return nil, err
}
for _, ip := range ips {
result := &network{}
link, err := netlink.LinkByName(iface)
if err != nil {
return networks, errors.Wrapf(err, "could not get link for interface '%s'", iface)
result := &network{
link: networkLink,
routeTable: tableID,
routingTableType: tableType,
routingProtocol: routingProtocol,
forwardMethod: forwardMethod,
iptablesBackend: iptablesBackend,
isDDNS: isDDNS,
dnsName: address,
ipvsEnabled: ipvsEnabled,
enableSecurity: enableSecurity,
}
result.link = link
result.routeTable = tableID
result.routingTableType = tableType
// address is DNS
result.isDDNS = isDDNS
result.dnsName = address
// we're able to resolve store this as the initial IP
if result.address, err = netlinkParse(ip); err != nil {
if result.address, err = netlink.ParseAddr(fmt.Sprintf("%s/%s", ip, subnet)); err != nil {
return networks, err
}
// set ValidLft so that the VIP expires if the DNS entry is updated, otherwise it'll be refreshed by the DNS prober
result.address.ValidLft = defaultValidLft
// set address as deprecated so it isn't used as source address according to RFC 3484
result.address.PreferedLft = 0
networks = append(networks, result)
}
@@ -182,7 +225,7 @@ func (configurator *network) PrepareRoute() *netlink.Route {
route := &netlink.Route{
Scope: routeScope,
Dst: configurator.address.IPNet,
LinkIndex: configurator.link.Attrs().Index,
LinkIndex: configurator.link.Intf.Attrs().Index,
Table: configurator.routeTable,
Type: configurator.routingTableType,
Protocol: netlink.RouteProtocol(configurator.routingProtocol),
@@ -191,9 +234,42 @@ func (configurator *network) PrepareRoute() *netlink.Route {
}
// AddRoute - Add an IP address to a route table
func (configurator *network) AddRoute() error {
func (configurator *network) AddRoute(precheck bool) error {
configurator.link.Lock.Lock()
defer configurator.link.Lock.Unlock()
route := configurator.PrepareRoute()
return netlink.RouteAdd(route)
exists := false
var err error
if precheck {
exists, err = configurator.routeExists(route)
if err != nil {
return errors.Wrap(err, "failed to check route")
}
}
if !exists {
if err := netlink.RouteAdd(route); err != nil {
return errors.Wrap(err, "failed to add route")
}
}
return nil
}
func (configurator *network) routeExists(route *netlink.Route) (bool, error) {
routes, err := netlink.RouteList(configurator.link.Intf, netlink.FAMILY_ALL)
if err != nil {
return false, errors.Wrap(err, "failed to list routes")
}
for _, r := range routes {
if r.Equal(*route) {
return true, nil
}
}
return false, nil
}
// DeleteRoute - Delete an IP address from a route table
@@ -232,17 +308,46 @@ func (configurator *network) UpdateRoutes() (bool, error) {
}
// AddIP - Add an IP address to the interface
func (configurator *network) AddIP() error {
if err := netlink.AddrReplace(configurator.link, configurator.address); err != nil {
return errors.Wrap(err, "could not add ip")
func (configurator *network) AddIP(precheck bool) (bool, error) {
configurator.link.Lock.Lock()
defer configurator.link.Lock.Unlock()
exists := false
var err error
if precheck {
if exists, err = configurator.IsSet(); err != nil {
return false, errors.Wrap(err, "could not check if address exists")
}
}
if os.Getenv("enable_service_security") == "true" && !configurator.ignoreSecurity {
if exists {
return false, nil
}
if err := netlink.AddrReplace(configurator.link.Intf, configurator.address); err != nil {
return false, errors.Wrap(err, "could not add ip")
}
if err := configurator.configureIPTables(); err != nil {
return true, errors.Wrap(err, "could not configure IPTables")
}
return true, nil
}
func (configurator *network) configureIPTables() error {
if configurator.enableSecurity && !configurator.ignoreSecurity {
if err := configurator.addIptablesRulesToLimitTrafficPorts(); err != nil {
return errors.Wrap(err, "could not add iptables rules to limit traffic ports")
}
}
// It seems that masquerading is only reuired with IPv4 for IPVS to work.
if configurator.ipvsEnabled && configurator.forwardMethod == "masquerade" && configurator.address.IP.To4() != nil {
if err := configurator.addIptablesRulesForMasquerade(); err != nil {
return errors.Wrap(err, "could not add iptables rules for masquerade")
}
}
return nil
}
@@ -257,7 +362,7 @@ func (configurator *network) addIptablesRulesToLimitTrafficPorts() error {
if err := insertCommonIPTablesRules(ipt, vip, comment); err != nil {
return fmt.Errorf("could not add common iptables rules: %w", err)
}
log.Debugf("add iptables rules, vip: %s, ports: %+v", vip, configurator.ports)
log.Debug("add iptables rules", "vip", vip, "ports", configurator.ports)
if err := configurator.insertIPTablesRulesForServicePorts(ipt, vip, comment); err != nil {
return fmt.Errorf("could not add iptables rules for service ports: %v", err)
}
@@ -362,7 +467,7 @@ func (configurator *network) removeIptablesRuleToLimitTrafficPorts() error {
return fmt.Errorf("could not delete common iptables rules: %w", err)
}
log.Debugf("remove iptables rules, vip: %s, ports: %+v", vip, configurator.ports)
log.Debug("remove iptables rules", "vip", vip, "ports", configurator.ports)
for _, port := range configurator.ports {
// iptables -D INPUT -d <VIP> -p <protocol> --dport <port> -j ACCEPT
if err := ipt.DeleteIfExists(iptables.TableFilter, iptables.ChainInput, "-d", vip, "-p", string(port.Protocol),
@@ -375,38 +480,112 @@ func (configurator *network) removeIptablesRuleToLimitTrafficPorts() error {
}
// DeleteIP - Remove an IP address from the interface
func (configurator *network) DeleteIP() error {
func (configurator *network) DeleteIP() (bool, error) {
configurator.link.Lock.Lock()
defer configurator.link.Lock.Unlock()
result, err := configurator.IsSet()
if err != nil {
return errors.Wrap(err, "ip check in DeleteIP failed")
return false, errors.Wrap(err, "ip check in DeleteIP failed")
}
// Nothing to delete
if !result {
return nil
return false, nil
}
if err = netlink.AddrDel(configurator.link, configurator.address); err != nil {
return errors.Wrap(err, "could not delete ip")
if err = netlink.AddrDel(configurator.link.Intf, configurator.address); err != nil {
return false, errors.Wrap(err, "could not delete ip")
}
if os.Getenv("enable_service_security") == "true" && !configurator.ignoreSecurity {
if configurator.enableSecurity && !configurator.ignoreSecurity {
if err := configurator.removeIptablesRuleToLimitTrafficPorts(); err != nil {
return errors.Wrap(err, "could not remove iptables rules to limit traffic ports")
return true, errors.Wrap(err, "could not remove iptables rules to limit traffic ports")
}
}
if configurator.ipvsEnabled && configurator.forwardMethod == "masquerade" && configurator.address.IP.To4() != nil {
if err := configurator.removeIptablesRulesForMasquerade(); err != nil {
return true, errors.Wrap(err, "could not remove iptables masquerade rules ")
}
}
return true, nil
}
func (configurator *network) addIptablesRulesForMasquerade() error {
ver, err := iptables.GetVersion()
if err != nil {
return errors.Wrap(err, "could not get iptables version")
}
ipt, err := iptables.New(iptables.EnableNFTables(ver.BackendMode == "nft"))
if err != nil {
return errors.Wrap(err, "could not create iptables client")
}
vip := configurator.address.IP.String()
comment := fmt.Sprintf(iptablesComment, vip)
if err := addMasqueradeRuleForVIP(ipt, vip, comment); err != nil {
return err
}
return nil
}
// addIptablesRulesForMasquerade add iptables rules for MASQUERADE
// insert example
func (configurator *network) removeIptablesRulesForMasquerade() error {
ver, err := iptables.GetVersion()
if err != nil {
return errors.Wrap(err, "could not get iptables version")
}
ipt, err := iptables.New(iptables.EnableNFTables(ver.BackendMode == "nft"))
if err != nil {
return errors.Wrap(err, "could not create iptables client")
}
vip := configurator.address.IP.String()
comment := fmt.Sprintf(iptablesComment, vip)
err = delMasqueradeRuleForVIP(ipt, vip, comment)
if err != nil {
return err
}
return nil
}
// TODO: investigate if adding "--vport <port>" would be better or not quite necessary
// After this rule is added, ipvs kernel module is also loaded
func addMasqueradeRuleForVIP(ipt *iptables.IPTables, vip, comment string) error {
err := ipt.InsertUnique(iptables.TableNat, iptables.ChainPOSTROUTING,
1, "-m", "ipvs", "--vaddr", vip, "-j", "MASQUERADE", "-m", "comment", "--comment", comment)
if err != nil {
return fmt.Errorf("could not add masquerade rule for VIP %s: %v", vip, err)
}
return nil
}
func delMasqueradeRuleForVIP(ipt *iptables.IPTables, vip, comment string) error {
err := ipt.DeleteIfExists(iptables.TableNat, iptables.ChainPOSTROUTING,
"-m", "ipvs", "--vaddr", vip, "-j", "MASQUERADE", "-m", "comment", "--comment", comment)
if err != nil {
return fmt.Errorf("could not del masquerade rule for VIP %s: %v", vip, err)
}
return nil
}
// IsDADFAIL - Returns true if the address is IPv6 and has DADFAILED flag
func (configurator *network) IsDADFAIL() bool {
configurator.link.Lock.Lock()
defer configurator.link.Lock.Unlock()
if configurator.address == nil || !IsIPv6(configurator.address.IP.String()) {
return false
}
// Get all the address
addresses, err := netlink.AddrList(configurator.link, netlink.FAMILY_V6)
addresses, err := netlink.AddrList(configurator.link.Intf, netlink.FAMILY_V6)
if err != nil {
return false
}
@@ -425,7 +604,7 @@ func addressHasDADFAILEDFlag(address netlink.Addr) bool {
return address.Flags&unix.IFA_F_DADFAILED != 0
}
// IsSet - Check to see if VIP is set
// isSet - Check to see if VIP is set
func (configurator *network) IsSet() (result bool, err error) {
var addresses []netlink.Addr
@@ -433,11 +612,11 @@ func (configurator *network) IsSet() (result bool, err error) {
return false, nil
}
addresses, err = netlink.AddrList(configurator.link, 0)
addresses, err = netlink.AddrList(configurator.link.Intf, 0)
if err != nil {
err = errors.Wrap(err, "could not list addresses")
return
return false, err
}
for _, address := range addresses {
@@ -454,13 +633,30 @@ func (configurator *network) SetIP(ip string) error {
configurator.mu.Lock()
defer configurator.mu.Unlock()
addr, err := netlinkParse(ip)
configurator.link.Lock.Lock()
defer configurator.link.Lock.Unlock()
if strings.Contains("/", ip) {
return fmt.Errorf("ip should not contain CIDR notation got: %s", ip)
}
ones, _ := configurator.address.Mask.Size()
cidr, err := utils.FormatIPWithSubnetMask(ip, strconv.Itoa(ones))
if err != nil {
return fmt.Errorf("could not format address '%s' with subnetMask '%s'", ip, strconv.Itoa(ones))
}
addr, err := netlink.ParseAddr(cidr)
if err != nil {
return err
}
if configurator.address != nil && configurator.IsDNS() {
addr.ValidLft = defaultValidLft
} else {
addr.ValidLft = math.MaxInt
}
// set address as deprecated so it isn't used as source address according to RFC 3484
addr.PreferedLft = 0
configurator.address = addr
return nil
}
@@ -484,6 +680,21 @@ func (configurator *network) IP() string {
return configurator.address.IP.String()
}
func (configurator *network) CIDR() string {
configurator.mu.Lock()
defer configurator.mu.Unlock()
return configurator.address.IPNet.String()
}
// IP - return the IP Address
func (configurator *network) IPisLinkLocal() bool {
configurator.mu.Lock()
defer configurator.mu.Unlock()
return configurator.address.IP.IsLinkLocalUnicast()
}
// DNSName return the configured dnsName when use DNS
func (configurator *network) DNSName() string {
return configurator.dnsName
@@ -509,19 +720,23 @@ func (configurator *network) DDNSHostName() string {
// Interface - return the Interface name
func (configurator *network) Interface() string {
return configurator.link.Attrs().Name
return configurator.link.Intf.Attrs().Name
}
func GarbageCollect(adapter, address string) (found bool, err error) {
func GarbageCollect(adapter, address string, intfMgr *networkinterface.Manager) (found bool, err error) {
// Get adapter
link, err := netlink.LinkByName(adapter)
if err != nil {
return true, errors.Wrapf(err, "could not get link for interface '%s'", adapter)
}
l := intfMgr.Get(link)
l.Lock.Lock()
defer l.Lock.Unlock()
// Get addresses on adapter
addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL)
addrs, err := netlink.AddrList(l.Intf, netlink.FAMILY_ALL)
if err != nil {
return false, err
}
@@ -533,10 +748,92 @@ func GarbageCollect(adapter, address string) (found bool, err error) {
found = true
// linting issue
existing := existing
if err = netlink.AddrDel(link, &existing); err != nil {
if err = netlink.AddrDel(l.Intf, &existing); err != nil {
return true, errors.Wrap(err, "could not delete ip")
}
}
}
return // Didn't find the address on the adapter
}
func (configurator *network) SetMask(mask string) error {
selectedMask, err := SelectSubnet(configurator.IP(), mask)
if err != nil {
return fmt.Errorf("failed to select mask %q: %w", mask, err)
}
m, err := strconv.Atoi(selectedMask)
if err != nil {
return err
}
size := 32
family := "IPv4"
if IsIPv6(configurator.IP()) {
size = 128
family = "IPv6"
}
if m > size {
return fmt.Errorf("provided CIDR mask '%d' is greater than the highest mask value for the %s family (%d)", m, family, size)
}
toSet := net.CIDRMask(m, size)
if toSet == nil {
return fmt.Errorf("failed to create mask /%d", m)
}
configurator.mu.Lock()
defer configurator.mu.Unlock()
configurator.address.Mask = toSet
return nil
}
func (configurator *network) SetHasEndpoints(value bool) {
log.Debug("setting HasEndpoints", "ip", configurator.IP(), "value", value)
configurator.hasEndpoints = value
}
func (configurator *network) HasEndpoints() bool {
log.Debug("getting HasEndpoints", "ip", configurator.IP(), "value", configurator.hasEndpoints)
return configurator.hasEndpoints
}
func (configurator *network) ARPName() string {
return fmt.Sprintf("%s-%s", configurator.CIDR(), configurator.Interface())
}
// SelectSubnet formats an IP address with the appropriate CIDR based on the input.
// The input SubnetMasks can be "32,128" (dual-stack), "32", "128" (SingleStack).
func SelectSubnet(rawIP string, subnetMasks string) (string, error) {
// Split the SubnetMasks input into DualStack or SingleStack
// If the input is "32,128", it will be split into ["32", "128"]
subnetMasksParts := strings.Split(subnetMasks, ",")
if len(subnetMasksParts) == 0 {
return "", fmt.Errorf("no subnetMasks provided got: %q", subnetMasks)
} else if len(subnetMasksParts) > 2 {
return "", fmt.Errorf("invalid subnetMasks provided got: %q", subnetMasks)
}
if slices.Contains(subnetMasksParts, "auto") {
return "", fmt.Errorf("auto subnet discovery only works for services: %q", subnetMasks)
}
// Parse the raw IP address
ip := net.ParseIP(rawIP)
if ip == nil {
return "", fmt.Errorf("invalid IP address: %s", rawIP)
}
if ip.To4() != nil {
return subnetMasksParts[0], nil
}
if ip.To16() != nil {
subnetMask := subnetMasksParts[0]
if len(subnetMasksParts) == 2 {
subnetMask = subnetMasksParts[1]
}
return subnetMask, nil
}
return "", fmt.Errorf("unable to select subnet mask for: IP %q and masks %q", rawIP, subnetMasks)
}

View File

@@ -135,9 +135,7 @@ func sendARP(iface *net.Interface, m *arpMessage) error {
Halen: m.hardwareAddressLength,
}
target := ethernetBroadcast
for i := 0; i < len(target); i++ { //nolint
ll.Addr[i] = target[i]
}
copy(ll.Addr[:], target)
b, err := m.bytes()
if err != nil {

144
pkg/vip/const.go Normal file
View File

@@ -0,0 +1,144 @@
package vip
// The below is taken from the net/internal package (why this is internal i'm not 100% sure)
// Protocol Numbers, Updated: 2017-10-13
const (
ProtocolIP = 0 // IPv4 encapsulation, pseudo protocol number
ProtocolHOPOPT = 0 // IPv6 Hop-by-Hop Option
ProtocolICMP = 1 // Internet Control Message
ProtocolIGMP = 2 // Internet Group Management
ProtocolGGP = 3 // Gateway-to-Gateway
ProtocolIPv4 = 4 // IPv4 encapsulation
ProtocolST = 5 // Stream
ProtocolTCP = 6 // Transmission Control
ProtocolCBT = 7 // CBT
ProtocolEGP = 8 // Exterior Gateway Protocol
ProtocolIGP = 9 // any private interior gateway (used by Cisco for their IGRP)
ProtocolBBNRCCMON = 10 // BBN RCC Monitoring
ProtocolNVPII = 11 // Network Voice Protocol
ProtocolPUP = 12 // PUP
ProtocolEMCON = 14 // EMCON
ProtocolXNET = 15 // Cross Net Debugger
ProtocolCHAOS = 16 // Chaos
ProtocolUDP = 17 // User Datagram
ProtocolMUX = 18 // Multiplexing
ProtocolDCNMEAS = 19 // DCN Measurement Subsystems
ProtocolHMP = 20 // Host Monitoring
ProtocolPRM = 21 // Packet Radio Measurement
ProtocolXNSIDP = 22 // XEROX NS IDP
ProtocolTRUNK1 = 23 // Trunk-1
ProtocolTRUNK2 = 24 // Trunk-2
ProtocolLEAF1 = 25 // Leaf-1
ProtocolLEAF2 = 26 // Leaf-2
ProtocolRDP = 27 // Reliable Data Protocol
ProtocolIRTP = 28 // Internet Reliable Transaction
ProtocolISOTP4 = 29 // ISO Transport Protocol Class 4
ProtocolNETBLT = 30 // Bulk Data Transfer Protocol
ProtocolMFENSP = 31 // MFE Network Services Protocol
ProtocolMERITINP = 32 // MERIT Internodal Protocol
ProtocolDCCP = 33 // Datagram Congestion Control Protocol
Protocol3PC = 34 // Third Party Connect Protocol
ProtocolIDPR = 35 // Inter-Domain Policy Routing Protocol
ProtocolXTP = 36 // XTP
ProtocolDDP = 37 // Datagram Delivery Protocol
ProtocolIDPRCMTP = 38 // IDPR Control Message Transport Proto
ProtocolTPPP = 39 // TP++ Transport Protocol
ProtocolIL = 40 // IL Transport Protocol
ProtocolIPv6 = 41 // IPv6 encapsulation
ProtocolSDRP = 42 // Source Demand Routing Protocol
ProtocolIPv6Route = 43 // Routing Header for IPv6
ProtocolIPv6Frag = 44 // Fragment Header for IPv6
ProtocolIDRP = 45 // Inter-Domain Routing Protocol
ProtocolRSVP = 46 // Reservation Protocol
ProtocolGRE = 47 // Generic Routing Encapsulation
ProtocolDSR = 48 // Dynamic Source Routing Protocol
ProtocolBNA = 49 // BNA
ProtocolESP = 50 // Encap Security Payload
ProtocolAH = 51 // Authentication Header
ProtocolINLSP = 52 // Integrated Net Layer Security TUBA
ProtocolNARP = 54 // NBMA Address Resolution Protocol
ProtocolMOBILE = 55 // IP Mobility
ProtocolTLSP = 56 // Transport Layer Security Protocol using Kryptonet key management
ProtocolSKIP = 57 // SKIP
ProtocolIPv6ICMP = 58 // ICMP for IPv6
ProtocolIPv6NoNxt = 59 // No Next Header for IPv6
ProtocolIPv6Opts = 60 // Destination Options for IPv6
ProtocolCFTP = 62 // CFTP
ProtocolSATEXPAK = 64 // SATNET and Backroom EXPAK
ProtocolKRYPTOLAN = 65 // Kryptolan
ProtocolRVD = 66 // MIT Remote Virtual Disk Protocol
ProtocolIPPC = 67 // Internet Pluribus Packet Core
ProtocolSATMON = 69 // SATNET Monitoring
ProtocolVISA = 70 // VISA Protocol
ProtocolIPCV = 71 // Internet Packet Core Utility
ProtocolCPNX = 72 // Computer Protocol Network Executive
ProtocolCPHB = 73 // Computer Protocol Heart Beat
ProtocolWSN = 74 // Wang Span Network
ProtocolPVP = 75 // Packet Video Protocol
ProtocolBRSATMON = 76 // Backroom SATNET Monitoring
ProtocolSUNND = 77 // SUN ND PROTOCOL-Temporary
ProtocolWBMON = 78 // WIDEBAND Monitoring
ProtocolWBEXPAK = 79 // WIDEBAND EXPAK
ProtocolISOIP = 80 // ISO Internet Protocol
ProtocolVMTP = 81 // VMTP
ProtocolSECUREVMTP = 82 // SECURE-VMTP
ProtocolVINES = 83 // VINES
ProtocolTTP = 84 // Transaction Transport Protocol
ProtocolIPTM = 84 // Internet Protocol Traffic Manager
ProtocolNSFNETIGP = 85 // NSFNET-IGP
ProtocolDGP = 86 // Dissimilar Gateway Protocol
ProtocolTCF = 87 // TCF
ProtocolEIGRP = 88 // EIGRP
ProtocolOSPFIGP = 89 // OSPFIGP
ProtocolSpriteRPC = 90 // Sprite RPC Protocol
ProtocolLARP = 91 // Locus Address Resolution Protocol
ProtocolMTP = 92 // Multicast Transport Protocol
ProtocolAX25 = 93 // AX.25 Frames
ProtocolIPIP = 94 // IP-within-IP Encapsulation Protocol
ProtocolSCCSP = 96 // Semaphore Communications Sec. Pro.
ProtocolETHERIP = 97 // Ethernet-within-IP Encapsulation
ProtocolENCAP = 98 // Encapsulation Header
ProtocolGMTP = 100 // GMTP
ProtocolIFMP = 101 // Ipsilon Flow Management Protocol
ProtocolPNNI = 102 // PNNI over IP
ProtocolPIM = 103 // Protocol Independent Multicast
ProtocolARIS = 104 // ARIS
ProtocolSCPS = 105 // SCPS
ProtocolQNX = 106 // QNX
ProtocolAN = 107 // Active Networks
ProtocolIPComp = 108 // IP Payload Compression Protocol
ProtocolSNP = 109 // Sitara Networks Protocol
ProtocolCompaqPeer = 110 // Compaq Peer Protocol
ProtocolIPXinIP = 111 // IPX in IP
ProtocolVRRP = 112 // Virtual Router Redundancy Protocol
ProtocolPGM = 113 // PGM Reliable Transport Protocol
ProtocolL2TP = 115 // Layer Two Tunneling Protocol
ProtocolDDX = 116 // D-II Data Exchange (DDX)
ProtocolIATP = 117 // Interactive Agent Transfer Protocol
ProtocolSTP = 118 // Schedule Transfer Protocol
ProtocolSRP = 119 // SpectraLink Radio Protocol
ProtocolUTI = 120 // UTI
ProtocolSMP = 121 // Simple Message Protocol
ProtocolPTP = 123 // Performance Transparency Protocol
ProtocolISIS = 124 // ISIS over IPv4
ProtocolFIRE = 125 // FIRE
ProtocolCRTP = 126 // Combat Radio Transport Protocol
ProtocolCRUDP = 127 // Combat Radio User Datagram
ProtocolSSCOPMCE = 128 // SSCOPMCE
ProtocolIPLT = 129 // IPLT
ProtocolSPS = 130 // Secure Packet Shield
ProtocolPIPE = 131 // Private IP Encapsulation within IP
ProtocolSCTP = 132 // Stream Control Transmission Protocol
ProtocolFC = 133 // Fibre Channel
ProtocolRSVPE2EIGNORE = 134 // RSVP-E2E-IGNORE
ProtocolMobilityHeader = 135 // Mobility Header
ProtocolUDPLite = 136 // UDPLite
ProtocolMPLSinIP = 137 // MPLS-in-IP
ProtocolMANET = 138 // MANET Protocols
ProtocolHIP = 139 // Host Identity Protocol
ProtocolShim6 = 140 // Shim6 Protocol
ProtocolWESP = 141 // Wrapped Encapsulating Security Payload
ProtocolROHC = 142 // Robust Header Compression
ProtocolReserved = 255 // Reserved
)

View File

@@ -5,8 +5,9 @@ import (
"net"
"time"
log "log/slog"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
// DDNSManager will start a dhclient to retrieve and keep the lease for the IP
@@ -55,7 +56,7 @@ func (ddns *ddnsManager) Start() (string, error) {
client.Stop()
return "", errors.New("failed to get IP from dhcp for ddns in 1 minutes")
case ip = <-client.IPChannel():
log.Info("got ip from dhcp: ", ip)
log.Info("got address from dhcp", "ip", ip)
}
// lease.FixedAddress.String() could return <nil>
@@ -74,7 +75,7 @@ func (ddns *ddnsManager) Start() (string, error) {
client.Stop()
return
case ip := <-client.IPChannel():
log.Info("got ip from dhcp: ", ip)
log.Info("got address from dhcp", "ip", ip)
}
}
}(ddns.ctx)

View File

@@ -8,10 +8,11 @@ import (
"net"
"time"
log "log/slog"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/jpillora/backoff"
log "github.com/sirupsen/logrus"
)
const dhcpClientPort = "68"
@@ -129,7 +130,7 @@ func (c *DHCPClient) Start() {
// Set up two ticker to renew/rebind regularly
t1Timeout := c.lease.ACK.IPAddressLeaseTime(defaultDHCPRenew) / 2
t2Timeout := (c.lease.ACK.IPAddressLeaseTime(defaultDHCPRenew) / 8) * 7
log.Debugf("t1 %v t2 %v", t1Timeout, t2Timeout)
log.Debug("dhcp timeouts", "timeout1", t1Timeout, "timeoute2", t2Timeout)
t1, t2 := time.NewTicker(t1Timeout), time.NewTicker(t2Timeout)
for {
@@ -143,25 +144,25 @@ func (c *DHCPClient) Start() {
lease, err := c.renew()
if err == nil {
c.lease = lease
log.Infof("renew, lease: %+v", lease)
log.Info("renew", "lease", lease)
t2.Reset(t2Timeout)
} else {
log.Errorf("renew failed, error: %s", err.Error())
log.Error("renew failed", "err", err)
}
case <-t2.C:
// rebind is just like a request, but forcing to provide a new IP address
lease, err := c.request(true)
if err == nil {
c.lease = lease
log.Infof("rebind, lease: %+v", lease)
log.Info("rebind", "lease", lease)
} else {
if _, ok := err.(*nclient4.ErrNak); !ok {
t1.Stop()
t2.Stop()
log.Errorf("rebind failed, error: %s", err.Error())
log.Error("rebind failed", "err", err)
return
}
log.Warnf("ip %s may have changed: %s", c.lease.ACK.YourIPAddr, err.Error())
log.Warn("ip may have changed", "ip", c.lease.ACK.YourIPAddr, "err", err)
c.initRebootFlag = false
c.lease = c.requestWithBackoff()
}
@@ -171,9 +172,9 @@ func (c *DHCPClient) Start() {
case <-c.stopChan:
// release is a unicast request of the IP release.
if err := c.release(); err != nil {
log.Errorf("release lease failed, error: %s, lease: %+v", err.Error(), c.lease)
log.Error("release lease failed", "lease", lease, "err", err)
} else {
log.Infof("release, lease: %+v", c.lease)
log.Info("release", "lease", lease)
}
t1.Stop()
t2.Stop()
@@ -205,18 +206,18 @@ func (c *DHCPClient) requestWithBackoff() *nclient4.Lease {
var err error
for {
log.Debugf("trying to get a new IP, attempt %f", backoff.Attempt())
log.Debug("trying to get a new IP", "attempt", backoff.Attempt())
lease, err = c.request(false)
if err != nil {
dur := backoff.Duration()
if backoff.Attempt() > maxBackoffAttempts-1 {
errMsg := fmt.Errorf("failed to get an IP address after %d attempts, error %s, giving up", maxBackoffAttempts, err.Error())
log.Error(errMsg)
log.Error(errMsg.Error())
c.errorChan <- errMsg
c.Stop()
return nil
}
log.Errorf("request failed, error: %s (waiting %v)", err.Error(), dur)
log.Error("request failed", "err", err.Error(), "waiting", dur)
time.Sleep(dur)
continue
}
@@ -225,7 +226,7 @@ func (c *DHCPClient) requestWithBackoff() *nclient4.Lease {
}
if c.ipChan != nil {
log.Debugf("using channel")
log.Debug("using channel")
c.ipChan <- lease.ACK.YourIPAddr.String()
}
@@ -251,13 +252,13 @@ func (c *DHCPClient) request(rebind bool) (*nclient4.Lease, error) {
// if initRebootFlag is set, this means we have an IP already set on c.requestedIP that should be used
if c.initRebootFlag {
log.Debugf("init-reboot ip %s", c.requestedIP)
log.Debug("init-reboot", "ip", c.requestedIP)
modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(c.requestedIP)))
}
// if this is a rebind, then the IP we should set is the one that already exists in lease
if rebind {
log.Debugf("rebinding ip %s", c.lease.ACK.YourIPAddr)
log.Debug("rebinding", "ip", c.lease.ACK.YourIPAddr)
modifiers = append(modifiers, dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(c.lease.ACK.YourIPAddr)))
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"time"
log "github.com/sirupsen/logrus"
log "log/slog"
)
// IPUpdater is the interface to plug dns updaters
@@ -29,7 +29,7 @@ func (d *ipUpdater) Run(ctx context.Context) {
for {
select {
case <-ctx.Done():
log.Infof("stop ipUpdater")
log.Info("stop ipUpdater")
return
default:
mode := "ipv4"
@@ -39,18 +39,18 @@ func (d *ipUpdater) Run(ctx context.Context) {
ip, err := LookupHost(d.vip.DNSName(), mode)
if err != nil {
log.Warnf("cannot lookup %s: %v", d.vip.DNSName(), err)
log.Warn("cannot lookup", "name", d.vip.DNSName(), "err", err)
// fallback to renewing the existing IP
ip = []string{d.vip.IP()}
}
log.Infof("setting %s as an IP", ip)
log.Info("setting IP", "address", ip)
if err := d.vip.SetIP(ip[0]); err != nil {
log.Errorf("setting %s as an IP: %v", ip, err)
log.Error("setting IP", "address", ip, "err", err)
}
if err := d.vip.AddIP(); err != nil {
log.Errorf("error adding virtual IP: %v", err)
if _, err := d.vip.AddIP(false); err != nil {
log.Error("error adding virtual IP", "err", err)
}
}

View File

@@ -2,10 +2,12 @@ package vip
import (
"fmt"
"strconv"
"strings"
log "log/slog"
iptables "github.com/kube-vip/kube-vip/pkg/iptables"
log "github.com/sirupsen/logrus"
ct "github.com/florianl/go-conntrack"
)
@@ -34,7 +36,11 @@ type Egress struct {
}
func CreateIptablesClient(nftables bool, namespace string, protocol iptables.Protocol) (*Egress, error) {
log.Infof("[egress] Creating an iptables client, nftables mode [%t]", nftables)
proto := "IPv4"
if protocol == iptables.ProtocolIPv6 {
proto = "IPv6"
}
log.Info("[egress] Creating an iptables client", "nftables", nftables, "protocol", proto)
e := new(Egress)
var err error
@@ -51,7 +57,7 @@ func CreateIptablesClient(nftables bool, namespace string, protocol iptables.Pro
}
func (e *Egress) CheckMangleChain(name string) (bool, error) {
log.Infof("[egress] Checking for Chain [%s]", name)
log.Info("[egress] chain exists", "name", name)
return e.ipTablesClient.ChainExists("mangle", name)
}
@@ -63,8 +69,12 @@ func (e *Egress) DeleteManglePrerouting(name string) error {
return e.ipTablesClient.Delete("mangle", "PREROUTING", "-j", name)
}
func (e *Egress) DeleteMangleReturnForNetwork(name, network string) error {
return e.ipTablesClient.Delete("mangle", name, "-d", network, "-j", "RETURN", "-m", "comment", "--comment", e.comment)
}
func (e *Egress) DeleteMangleMarking(podIP, name string) error {
log.Infof("[egress] Stopping marking packets on network [%s]", podIP)
log.Info("[egress] Stopping marking packets on network", "podIP", podIP)
exists, _ := e.ipTablesClient.Exists("mangle", name, "-s", podIP, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
@@ -74,8 +84,19 @@ func (e *Egress) DeleteMangleMarking(podIP, name string) error {
return e.ipTablesClient.Delete("mangle", name, "-s", podIP, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
}
func (e *Egress) DeleteMangleMarkingForNetwork(podIP, name, network string) error {
log.Info("[egress] Stopping marking packets", "podIP", podIP, "network", network)
// exists, _ := e.ipTablesClient.Exists("mangle", name, "-s", podIP, "-d", network, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
// if !exists {
// return fmt.Errorf("unable to find source Mangle rule for [%s]", podIP)
// }
return e.ipTablesClient.Delete("mangle", name, "-s", podIP, "-d", network, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
}
func (e *Egress) DeleteSourceNat(podIP, vip string) error {
log.Infof("[egress] Removing source nat from [%s] => [%s]", podIP, vip)
log.Info("[egress] Removing source nat", "podIP", podIP, "vip", vip)
exists, _ := e.ipTablesClient.Exists("nat", "POSTROUTING", "-s", podIP+"/32", "-m", "mark", "--mark", "64/64", "-j", "SNAT", "--to-source", vip, "-m", "comment", "--comment", e.comment)
@@ -86,7 +107,7 @@ func (e *Egress) DeleteSourceNat(podIP, vip string) error {
}
func (e *Egress) DeleteSourceNatForDestinationPort(podIP, vip, port, proto string) error {
log.Infof("[egress] Adding source nat from [%s] => [%s]", podIP, vip)
log.Info("[egress] Removing source nat", "podIP", podIP, "vip", vip, "destination port", port)
exists, _ := e.ipTablesClient.Exists("nat", "POSTROUTING", "-s", podIP+"/32", "-m", "mark", "--mark", "64/64", "-j", "SNAT", "--to-source", vip, "-p", proto, "--dport", port, "-m", "comment", "--comment", e.comment)
@@ -98,13 +119,13 @@ func (e *Egress) DeleteSourceNatForDestinationPort(podIP, vip, port, proto strin
func (e *Egress) CreateMangleChain(name string) error {
log.Infof("[egress] Creating Chain [%s]", name)
log.Info("[egress] Creating Chain", "name", name)
// Creates a new chain in the mangle table
return e.ipTablesClient.NewChain("mangle", name)
}
func (e *Egress) AppendReturnRulesForDestinationSubnet(name, subnet string) error {
log.Infof("[egress] Adding jump for subnet [%s] to RETURN to previous chain/rules", subnet)
log.Info("[egress] Adding jump for subnet to RETURN to previous chain/rules", "subnet", subnet)
exists, _ := e.ipTablesClient.Exists("mangle", name, "-d", subnet, "-j", "RETURN", "-m", "comment", "--comment", e.comment)
if !exists {
return e.ipTablesClient.Append("mangle", name, "-d", subnet, "-j", "RETURN", "-m", "comment", "--comment", e.comment)
@@ -113,7 +134,7 @@ func (e *Egress) AppendReturnRulesForDestinationSubnet(name, subnet string) erro
}
func (e *Egress) AppendReturnRulesForMarking(name, subnet string) error {
log.Infof("[egress] Marking packets on network [%s]", subnet)
log.Info("[egress] Marking packets on network", "subnet", subnet)
exists, _ := e.ipTablesClient.Exists("mangle", name, "-s", subnet, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
if !exists {
return e.ipTablesClient.Append("mangle", name, "-s", subnet, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
@@ -121,8 +142,17 @@ func (e *Egress) AppendReturnRulesForMarking(name, subnet string) error {
return nil
}
func (e *Egress) AppendReturnRulesForMarkingForNetwork(name, subnet, destination string) error {
log.Info("[egress] Marking packets on network", "subnet", subnet)
exists, _ := e.ipTablesClient.Exists("mangle", name, "-s", subnet, "-d", destination, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
if !exists {
return e.ipTablesClient.Append("mangle", name, "-s", subnet, "-d", destination, "-j", "MARK", "--set-mark", "64/64", "-m", "comment", "--comment", e.comment)
}
return nil
}
func (e *Egress) InsertMangeTableIntoPrerouting(name string) error {
log.Infof("[egress] Adding jump from mangle prerouting to [%s]", name)
log.Info("[egress] Adding jump from mangle prerouting", "destination", name)
if exists, err := e.ipTablesClient.Exists("mangle", "PREROUTING", "-j", name, "-m", "comment", "--comment", e.comment); err != nil {
return err
} else if exists {
@@ -135,7 +165,7 @@ func (e *Egress) InsertMangeTableIntoPrerouting(name string) error {
}
func (e *Egress) InsertSourceNat(vip, podIP string) error {
log.Infof("[egress] Adding source nat from [%s] => [%s]", podIP, vip)
log.Info("[egress] Adding source nat", "original source", podIP, "new source", vip)
if exists, err := e.ipTablesClient.Exists("nat", "POSTROUTING", "-s", podIP+"/32", "-m", "mark", "--mark", "64/64", "-j", "SNAT", "--to-source", vip, "-m", "comment", "--comment", e.comment); err != nil {
return err
} else if exists {
@@ -148,7 +178,20 @@ func (e *Egress) InsertSourceNat(vip, podIP string) error {
}
func (e *Egress) InsertSourceNatForDestinationPort(vip, podIP, port, proto string) error {
log.Infof("[egress] Adding source nat from [%s] => [%s], with destination port [%s]", podIP, vip, port)
log.Info("[egress] Adding source nat", "from", podIP, "to", vip, "port", port)
natRules, err := e.ipTablesClient.List("nat", "POSTROUTING")
if err != nil {
return err
}
foundNatRules := e.findExistingVIP(natRules, vip)
log.Warn("[egress] Cleaning existing postrouting nat rules for vip", "rulecount", len(foundNatRules), "vip", vip)
for x := range foundNatRules {
err = e.ipTablesClient.Delete("nat", "POSTROUTING", foundNatRules[x][2:]...)
if err != nil {
log.Error("[egress] removing rule", "err", err)
}
}
if exists, err := e.ipTablesClient.Exists("nat", "POSTROUTING", "-s", podIP+"/32", "-m", "mark", "--mark", "64/64", "-j", "SNAT", "--to-source", vip, "-p", proto, "--dport", port, "-m", "comment", "--comment", e.comment); err != nil {
return err
} else if exists {
@@ -160,29 +203,90 @@ func (e *Egress) InsertSourceNatForDestinationPort(vip, podIP, port, proto strin
return e.ipTablesClient.Insert("nat", "POSTROUTING", 1, "-s", podIP+"/32", "-m", "mark", "--mark", "64/64", "-j", "SNAT", "--to-source", vip, "-p", proto, "--dport", port, "-m", "comment", "--comment", e.comment)
}
func DeleteExistingSessions(sessionIP string, destination bool) error {
func DeleteExistingSessions(sessionIP string, destination bool, destinationPorts, srcPorts string) error {
nfct, err := ct.Open(&ct.Config{})
if err != nil {
log.Errorf("could not create nfct: %v", err)
log.Error("create conntrack client", "err", err)
return err
}
defer nfct.Close()
sessions, err := nfct.Dump(ct.Conntrack, ct.IPv4)
if err != nil {
log.Errorf("could not dump sessions: %v", err)
log.Error("could not dump sessions", "err", err)
return err
}
destPortProtocol := make(map[uint16]uint8)
srcPortProtocol := make(map[uint16]uint8)
if destinationPorts != "" {
fixedPorts := strings.Split(destinationPorts, ",")
for _, fixedPort := range fixedPorts {
data := strings.Split(fixedPort, ":")
if len(data) == 0 {
continue
}
port, err := strconv.ParseUint(data[1], 10, 16)
if err != nil {
return fmt.Errorf("[egress] error parsing annotaion [%s]", destinationPorts)
}
switch data[0] {
case strings.ToLower("udp"):
destPortProtocol[uint16(port)] = ProtocolUDP
case strings.ToLower("tcp"):
destPortProtocol[uint16(port)] = ProtocolTCP
case strings.ToLower("sctp"):
destPortProtocol[uint16(port)] = ProtocolSCTP
default:
log.Error("[egress] annotation protocol isn't supported", "protocol ID", data[0])
}
}
}
if srcPorts != "" {
fixedPorts := strings.Split(srcPorts, ",")
for _, fixedPort := range fixedPorts {
data := strings.Split(fixedPort, ":")
if len(data) == 0 {
continue
}
port, err := strconv.ParseUint(data[1], 10, 16)
if err != nil {
return fmt.Errorf("[egress] error parsing annotaion [%s]", srcPorts)
}
switch data[0] {
case strings.ToLower("udp"):
srcPortProtocol[uint16(port)] = ProtocolUDP
case strings.ToLower("tcp"):
srcPortProtocol[uint16(port)] = ProtocolTCP
case strings.ToLower("sctp"):
srcPortProtocol[uint16(port)] = ProtocolSCTP
default:
log.Error("[egress] annotation protocol isn't supported", "protocol ID", data[0])
}
}
}
// by default we only clear source (i.e. connections going from the vip (egress))
if !destination {
for _, session := range sessions {
//fmt.Printf("Looking for [%s] found [%s]\n", podIP, session.Origin.Dst.String())
//session.Origin.Proto
if session.Origin.Src.String() == sessionIP /*&& *session.Origin.Proto.DstPort == uint16(destinationPort)*/ {
//fmt.Printf("Source -> %s Destination -> %s:%d\n", session.Origin.Src.String(), session.Origin.Dst.String(), *session.Origin.Proto.DstPort)
err = nfct.Delete(ct.Conntrack, ct.IPv4, session)
if destinationPorts != "" {
proto := destPortProtocol[*session.Origin.Proto.DstPort]
if proto == *session.Origin.Proto.Number {
log.Info("[egress] cleaning existing connection", "src", session.Origin.Src.String(), "dst", session.Origin.Dst.String(), "dst port", *session.Origin.Proto.DstPort, "protocol", *session.Origin.Proto.Number)
err = nfct.Delete(ct.Conntrack, ct.IPv4, session)
}
} else {
err = nfct.Delete(ct.Conntrack, ct.IPv4, session)
}
if err != nil {
log.Errorf("could not delete sessions: %v", err)
log.Error("could not delete sessions", "err", err)
}
}
}
@@ -192,10 +296,17 @@ func DeleteExistingSessions(sessionIP string, destination bool) error {
//fmt.Printf("Looking for [%s] found [%s]\n", podIP, session.Origin.Dst.String())
if session.Origin.Dst.String() == sessionIP /*&& *session.Origin.Proto.DstPort == uint16(destinationPort)*/ {
//fmt.Printf("Source -> %s Destination -> %s:%d\n", session.Origin.Src.String(), session.Origin.Dst.String(), *session.Origin.Proto.DstPort)
err = nfct.Delete(ct.Conntrack, ct.IPv4, session)
if srcPorts != "" {
proto := srcPortProtocol[*session.Origin.Proto.DstPort]
if proto == *session.Origin.Proto.Number {
log.Info("[egress] cleaning existing connection", "src", session.Origin.Src.String(), "dst", session.Origin.Dst.String(), "dst port", *session.Origin.Proto.DstPort, "protocol", *session.Origin.Proto.Number)
err = nfct.Delete(ct.Conntrack, ct.IPv4, session)
}
} else {
err = nfct.Delete(ct.Conntrack, ct.IPv4, session)
}
if err != nil {
log.Errorf("could not delete sessions: %v", err)
log.Error("could not delete sessions", "err", err)
}
}
}
@@ -207,13 +318,13 @@ func DeleteExistingSessions(sessionIP string, destination bool) error {
// Debug functions
func (e *Egress) DumpChain(name string) error {
log.Infof("Dumping chain [%s]", name)
log.Info("Dumping chain", "name", name)
c, err := e.ipTablesClient.List("mangle", name)
if err != nil {
return err
}
for x := range c {
log.Infof("Rule -> %s", c[x])
log.Info("", "rule", c[x])
}
return nil
}
@@ -224,16 +335,16 @@ func (e *Egress) CleanIPtables() error {
return err
}
foundNatRules := e.findRules(natRules)
log.Warnf("[egress] Cleaning [%d] dangling postrouting nat rules", len(foundNatRules))
log.Warn("[egress] Cleaning dangling postrouting nat rules", "rulecount", len(foundNatRules))
for x := range foundNatRules {
err = e.ipTablesClient.Delete("nat", "POSTROUTING", foundNatRules[x][2:]...)
if err != nil {
log.Errorf("[egress] Error removing rule [%v]", err)
log.Error("[egress] Error removing rule", "err", err)
}
}
exists, err := e.CheckMangleChain(MangleChainName)
if err != nil {
log.Debugf("[egress] No Mangle chain exists [%v]", err)
log.Debug("[egress] No Mangle chain exists", "err", err)
}
if exists {
mangleRules, err := e.ipTablesClient.List("mangle", MangleChainName)
@@ -241,11 +352,11 @@ func (e *Egress) CleanIPtables() error {
return err
}
foundNatRules = e.findRules(mangleRules)
log.Warnf("[egress] Cleaning [%d] dangling prerouting mangle rules", len(foundNatRules))
log.Warn("[egress] Cleaning dangling prerouting mangle rules", "rulecount", len(foundNatRules))
for x := range foundNatRules {
err = e.ipTablesClient.Delete("mangle", MangleChainName, foundNatRules[x][2:]...)
if err != nil {
log.Errorf("[egress] Error removing rule [%v]", err)
log.Error("[egress] Error removing rule", "err", err)
}
}
@@ -257,7 +368,7 @@ func (e *Egress) CleanIPtables() error {
// log.Errorf("[egress] Error removing flushing table [%v]", err)
// }
} else {
log.Warnf("No existing mangle chain [%s] exists", MangleChainName)
log.Warn("No existing mangle chain exists", "chain name", MangleChainName)
}
return nil
}
@@ -278,3 +389,33 @@ func (e *Egress) findRules(rules []string) [][]string {
return foundRules
}
func (e *Egress) findExistingVIP(rules []string, vip string) [][]string {
var foundRules [][]string
for i := range rules {
r := strings.Split(rules[i], " ")
for x := range r {
// Look for a vip already in a post Routing rule
if r[x] == vip {
foundRules = append(foundRules, r)
}
}
}
return foundRules
}
func ClearIPTables(useNftables bool, namespace string, protocol iptables.Protocol) {
i, err := CreateIptablesClient(useNftables, namespace, protocol)
if err != nil {
log.Warn("[egress] Unable to clean any dangling egress rules", "err", 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.Error("Error cleaning rules", "err", cleanErr)
}
}
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/mdlayher/ndp"
log "github.com/sirupsen/logrus"
log "log/slog"
)
// NdpResponder defines the parameters for the NDP connection.
@@ -50,7 +50,7 @@ func (n *NdpResponder) SendGratuitous(address string) error {
return fmt.Errorf("failed to parse address %s", ip)
}
log.Infof("Broadcasting NDP update for %s (%s) via %s", address, n.hardwareAddr, n.intf)
log.Info("Broadcasting NDP update", "ip", address, "hwaddr", n.hardwareAddr, "interface", n.intf)
return n.advertise(netip.IPv6LinkLocalAllNodes(), ip, true)
}
@@ -66,6 +66,7 @@ func (n *NdpResponder) advertise(dst, target netip.Addr, gratuitous bool) error
},
},
}
log.Infof("ndp: %v", m)
log.Debug("ndp", "advertisement", m)
return n.conn.WriteTo(m, nil, dst)
}

View File

@@ -8,8 +8,9 @@ import (
"strings"
"syscall"
log "log/slog"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
)
@@ -104,15 +105,20 @@ func IsIPv6(address string) bool {
return ip.To4() == nil
}
// GetFullMask returns /32 for an IPv4 address and /128 for an IPv6 address
func GetFullMask(address string) (string, error) {
if IsIPv4(address) {
return "/32", nil
func IsIPv4CIDR(cidr string) bool {
ip, _, _ := net.ParseCIDR(cidr)
if ip == nil {
return false
}
if IsIPv6(address) {
return "/128", nil
return ip.To4() != nil
}
func IsIPv6CIDR(cidr string) bool {
ip, _, _ := net.ParseCIDR(cidr)
if ip == nil {
return false
}
return "", fmt.Errorf("failed to parse %s as either IPv4 or IPv6", address)
return ip.To4() == nil
}
// GetDefaultGatewayInterface return default gateway interface link
@@ -151,7 +157,7 @@ func MonitorDefaultInterface(ctx context.Context, defaultIF *net.Interface) erro
for {
select {
case r := <-routeCh:
log.Debugf("type: %d, route: %+v", r.Type, r.Route)
log.Debug(fmt.Sprintf("type: %d, route: %+v", r.Type, r.Route))
if r.Type == syscall.RTM_DELROUTE && (r.Dst == nil || r.Dst.String() == "0.0.0.0/0") && r.LinkIndex == defaultIF.Index {
return fmt.Errorf("default route deleted and the default interface may be invalid")
}
@@ -175,15 +181,61 @@ func GenerateMac() (mac string) {
* - https://macaddress.io/database-download
*/
mac = fmt.Sprintf("%s:%s:%s:%02x:%02x:%02x", "00", "00", "6C", buf[0], buf[1], buf[2])
log.Infof("Generated mac: %s", mac)
log.Info("Generated mac", "address", mac)
return mac
}
func GetIPs(vip string) []string {
addresses := []string{}
vips := strings.Split(vip, ",")
for _, v := range vips {
addresses = append(addresses, strings.TrimSpace(v))
func Split(values string) []string {
result := strings.Split(values, ",")
for i := range result {
result[i] = strings.TrimSpace(result[i])
}
return addresses
return result
}
// GetInterfaceByIP returns the network interface that has the specified IP address assigned.
func GetInterfaceByIP(ipAddr string) (*netlink.Link, error) {
ip := net.ParseIP(ipAddr)
if ip == nil {
return nil, fmt.Errorf("invalid IP address: %s", ipAddr)
}
links, err := netlink.LinkList()
if err != nil {
return nil, fmt.Errorf("failed to list network interfaces: %v", err)
}
for i := range links {
addrs, err := netlink.AddrList(links[i], netlink.FAMILY_ALL)
if err != nil {
return nil, fmt.Errorf("failed to list addresses for interface %s: %v", links[i].Attrs().Name, err)
}
for _, addr := range addrs {
if addr.IP.Equal(ip) {
return &links[i], nil
}
}
}
return nil, fmt.Errorf("no interface found with IP address: %s", ipAddr)
}
// GetNonLinkLocalIP returns the first non link-local IPv4/IPv6 address on the given interface.
func GetNonLinkLocalIP(iface *netlink.Link, family int) (string, error) {
a, err := netlink.AddrList(*iface, family)
if err != nil {
return "", fmt.Errorf("failed to list addresses for interface %s: %v", (*iface).Attrs().Name, err)
}
for _, addr := range a {
if addr.IPNet != nil {
ip := addr.IPNet.IP
if !ip.IsLinkLocalUnicast() {
return ip.String(), nil
}
}
}
return "", fmt.Errorf("failed to find non-local IP on interface: %s", (*iface).Attrs().Name)
}

13
test.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/bash
rm -f logs.txt
for i in $(seq 1 100);
do
echo "RUN $i"
GOMAXPROCS=4 make e2e-tests129
if [[ "$?" -ne 0 ]]; then
echo "FAILED AT RUN $i"
break
fi
done

View File

@@ -0,0 +1,3 @@
[global.config]
as = {{ .AS }}
router-id = "1.1.1.1"

View File

@@ -9,7 +9,7 @@ import (
"strings"
"time"
log "github.com/sirupsen/logrus"
log "log/slog"
)
func main() {
@@ -34,26 +34,43 @@ func main() {
if !exists {
log.Fatal("The environment variable E2EADDRESS, was not set")
}
ip := net.ParseIP(address)
network := "tcp"
port := ":12345"
if ip.To4() == nil {
network = "tcp6"
address = fmt.Sprintf("[%s]", address)
port = ":12346" // use a different port for IPv6 incase the IPv4 port is left connected
log.Infoln("Connecting with IPv6")
}
for {
// Connect to e2e endpoint with a second timeout
conn, err := net.DialTimeout("tcp", address+":12345", time.Second)
conn, err := net.DialTimeout(network, address+port, time.Second)
if err != nil {
log.Fatalf("Dial failed: %v", err.Error())
log.Errorf("Dial failed: %v", err.Error())
// Wait for a second and connect again
time.Sleep(time.Second)
continue
}
_, err = conn.Write([]byte("The Grid, a digital frontier"))
if err != nil {
log.Fatalf("Write data failed: %v ", err.Error())
log.Errorf("Write data failed: %v ", err.Error())
// Wait for a second and connect again
time.Sleep(time.Second)
continue
}
// buffer to get data
received := make([]byte, 1024)
_, err = conn.Read(received)
if err != nil {
log.Fatalf("Read data failed:", err.Error())
log.Errorf("Read data failed: %v", err.Error())
// Wait for a second and connect again
time.Sleep(time.Second)
continue
}
println("Received message: %s", string(received))
log.Infof("Received message: %s\n", string(received))
conn.Close()
// Wait for a second and connect again

683
testing/e2e/e2e_bgp_test.go Normal file
View File

@@ -0,0 +1,683 @@
//go:build e2e
// +build e2e
package e2e_test
import (
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"
"text/template"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
kindconfigv1alpha4 "sigs.k8s.io/kind/pkg/apis/config/v1alpha4"
"sigs.k8s.io/kind/pkg/log"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/kube-vip/kube-vip/pkg/vip"
"github.com/kube-vip/kube-vip/testing/e2e"
"github.com/kube-vip/kube-vip/testing/services/pkg/deployment"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
api "github.com/osrg/gobgp/v3/api"
)
const (
goBGPAS uint32 = 65500
kubevipAS uint32 = 65501
goBGPPort uint32 = 50051
defaultFixedNexthopv6 = "fc00:1000:1000:1000::100"
defaultFixedNexthopv4 = "172.18.0.100"
)
var _ = Describe("kube-vip BGP mode", Ordered, func() {
if Mode == ModeBGP {
var (
logger log.Logger
imagePath string
k8sImagePath string
configPath string
kubeVIPBGPManifestTemplate *template.Template
goBGPConfigTemplate *template.Template
tempDirPath string
v129 bool
localIPv4 string
localIPv6 string
curDir string
networkInterface string
bgpKill chan any
)
BeforeAll(func() {
klog.SetOutput(GinkgoWriter)
logger = e2e.TestLogger{}
imagePath = os.Getenv("E2E_IMAGE_PATH") // Path to kube-vip image
configPath = os.Getenv("CONFIG_PATH") // path to the api server config
k8sImagePath = os.Getenv("K8S_IMAGE_PATH") // path to the kubernetes image (version for kind)
if configPath == "" {
configPath = "/etc/kubernetes/admin.conf"
}
if networkInterface = os.Getenv("NETWORK_INTERFACE"); networkInterface == "" {
networkInterface = "br-"
}
_, v129 = os.LookupEnv("V129")
var err error
curDir, err = os.Getwd()
Expect(err).NotTo(HaveOccurred())
templateBGPPath := filepath.Join(curDir, "kube-vip-bgp.yaml.tmpl")
kubeVIPBGPManifestTemplate, err = template.New("kube-vip-bgp.yaml.tmpl").ParseFiles(templateBGPPath)
Expect(err).NotTo(HaveOccurred())
tempDirPath, err = os.MkdirTemp("", "kube-vip-test")
Expect(err).NotTo(HaveOccurred())
v4addr, _, err := deployment.GetLocalIPv4(networkInterface)
Expect(err).ToNot(HaveOccurred())
localIPv4 = v4addr.String()
v6addr, _, err := deployment.GetLocalIPv6(networkInterface)
Expect(err).ToNot(HaveOccurred())
localIPv6 = v6addr.String()
goBGPConfig := &e2e.BGPPeerValues{
AS: goBGPAS,
}
bgpKill = make(chan any)
goBGPConfigPath := filepath.Join(filepath.Join(curDir, "bgp"), "config.toml.tmpl")
goBGPConfigTemplate, err = template.New("config.toml.tmpl").ParseFiles(goBGPConfigPath)
Expect(err).ToNot(HaveOccurred())
goBGPConfigPath = filepath.Join(tempDirPath, "config.toml")
f, err := os.OpenFile(goBGPConfigPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
Expect(err).ToNot(HaveOccurred())
defer f.Close()
err = goBGPConfigTemplate.Execute(f, goBGPConfig)
Expect(err).ToNot(HaveOccurred())
go startGoBGP(goBGPConfigPath, bgpKill)
})
AfterAll(func() {
close(bgpKill)
})
Describe("kube-vip IPv4 services BGP mode functionality", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
gobgpClient api.GobgpApiClient
gobgpPeers []*e2e.BGPPeerValues
nodesNumber = 1
)
BeforeAll(func() {
setupEnv(&tempDirPath, &cpVIP, &clusterName, manifestValues, localIPv4, localIPv6, imagePath, configPath,
k8sImagePath, e2e.IPv4Family, e2e.IPv4Family, []string{e2e.IPv4Family}, &client, &gobgpPeers, v129,
kubeVIPBGPManifestTemplate, &gobgpClient, logger, nodesNumber, "", "bgp-ipv4")
})
AfterAll(func() {
for _, p := range gobgpPeers {
_, err := gobgpClient.DeletePeer(context.TODO(), &api.DeletePeerRequest{
Address: p.IP,
})
Expect(err).ToNot(HaveOccurred())
}
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("advertise IPv4 routes for services",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv4Family, api.Family_AFI_IP, []corev1.IPFamily{corev1.IPv4Protocol}, svcName, trafficPolicy, client, 1, gobgpClient, "")
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only stops advertising route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv4Family, api.Family_AFI_IP, []corev1.IPFamily{corev1.IPv4Protocol}, svcName, trafficPolicy, client, 2, gobgpClient, "")
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip IPv6 services BGP mode functionality", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
gobgpClient api.GobgpApiClient
gobgpPeers []*e2e.BGPPeerValues
nodesNumber = 1
)
BeforeAll(func() {
setupEnv(&tempDirPath, &cpVIP, &clusterName, manifestValues, localIPv4, localIPv6, imagePath, configPath,
k8sImagePath, e2e.IPv6Family, e2e.IPv6Family, []string{e2e.IPv6Family}, &client, &gobgpPeers, v129,
kubeVIPBGPManifestTemplate, &gobgpClient, logger, nodesNumber, "", "bgp-ipv4")
})
AfterAll(func() {
for _, p := range gobgpPeers {
_, err := gobgpClient.DeletePeer(context.TODO(), &api.DeletePeerRequest{
Address: p.IP,
})
Expect(err).ToNot(HaveOccurred())
}
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("advertise IPv6 routes for services",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv6Family, api.Family_AFI_IP6, []corev1.IPFamily{corev1.IPv6Protocol}, svcName, trafficPolicy, client, 1, gobgpClient, "")
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only stops advertising route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv6Family, api.Family_AFI_IP6, []corev1.IPFamily{corev1.IPv6Protocol}, svcName, trafficPolicy, client, 2, gobgpClient, "")
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv6 over IPv4 - fixed nexthop", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
gobgpClient api.GobgpApiClient
gobgpPeers []*e2e.BGPPeerValues
nodesNumber = 1
)
BeforeAll(func() {
setupEnv(&tempDirPath, &cpVIP, &clusterName, manifestValues, localIPv4, localIPv6, imagePath, configPath, k8sImagePath,
e2e.DualstackFamily, e2e.IPv4Family, []string{e2e.IPv4Family}, &client, &gobgpPeers, v129, kubeVIPBGPManifestTemplate, &gobgpClient,
logger, nodesNumber, "fixed", "mpbgp-ipv4")
})
AfterAll(func() {
for _, p := range gobgpPeers {
_, err := gobgpClient.DeletePeer(context.TODO(), &api.DeletePeerRequest{
Address: p.IP,
})
Expect(err).ToNot(HaveOccurred())
}
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("advertise IPv6 routes over IPv4 session",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv6Family, api.Family_AFI_IP6, []corev1.IPFamily{corev1.IPv6Protocol}, svcName, trafficPolicy, client, 1, gobgpClient, defaultFixedNexthopv6)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only stops advertising route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv6Family, api.Family_AFI_IP6, []corev1.IPFamily{corev1.IPv6Protocol}, svcName, trafficPolicy, client, 2, gobgpClient, defaultFixedNexthopv6)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - fixed nexthop", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
gobgpClient api.GobgpApiClient
gobgpPeers []*e2e.BGPPeerValues
nodesNumber = 1
)
BeforeAll(func() {
setupEnv(&tempDirPath, &cpVIP, &clusterName, manifestValues, localIPv4, localIPv6, imagePath, configPath, k8sImagePath,
e2e.DualstackFamilyIPv6, e2e.IPv6Family, []string{e2e.IPv6Family}, &client, &gobgpPeers, v129, kubeVIPBGPManifestTemplate, &gobgpClient,
logger, nodesNumber, "fixed", "mpbgp-ipv6")
})
AfterAll(func() {
for _, n := range gobgpPeers {
_, err := gobgpClient.DeletePeer(context.TODO(), &api.DeletePeerRequest{
Address: n.IP,
})
Expect(err).ToNot(HaveOccurred())
}
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("advertise IPv4 routes over IPv6 session",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv4Family, api.Family_AFI_IP, []corev1.IPFamily{corev1.IPv4Protocol}, svcName, trafficPolicy, client, 1, gobgpClient, defaultFixedNexthopv4)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only stops advertising route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv4Family, api.Family_AFI_IP, []corev1.IPFamily{corev1.IPv4Protocol}, svcName, trafficPolicy, client, 2, gobgpClient, defaultFixedNexthopv4)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv6 over IPv4 - auto_sourceif nexthop", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
gobgpClient api.GobgpApiClient
gobgpPeers []*e2e.BGPPeerValues
containerIP string
nodesNumber = 1
)
BeforeAll(func() {
_, containerIP = setupEnv(&tempDirPath, &cpVIP, &clusterName, manifestValues, localIPv4, localIPv6, imagePath, configPath, k8sImagePath,
e2e.DualstackFamily, e2e.IPv4Family, []string{e2e.IPv4Family}, &client, &gobgpPeers, v129, kubeVIPBGPManifestTemplate, &gobgpClient,
logger, nodesNumber, "auto_sourceif", "mpbgp-if-ipv4")
})
AfterAll(func() {
for _, p := range gobgpPeers {
_, err := gobgpClient.DeletePeer(context.TODO(), &api.DeletePeerRequest{
Address: p.IP,
})
Expect(err).ToNot(HaveOccurred())
}
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("advertise IPv6 routes over IPv4 session",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv6Family, api.Family_AFI_IP6, []corev1.IPFamily{corev1.IPv6Protocol}, svcName, trafficPolicy, client, 1, gobgpClient, containerIP)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only stops advertising route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv6Family, api.Family_AFI_IP6, []corev1.IPFamily{corev1.IPv6Protocol}, svcName, trafficPolicy, client, 2, gobgpClient, containerIP)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip DualStack services BGP mode functionality with MP-BGP IPv4 over IPv6 - fixed nexthop", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
gobgpClient api.GobgpApiClient
gobgpPeers []*e2e.BGPPeerValues
containerIP string
nodesNumber = 1
)
BeforeAll(func() {
containerIP, _ = setupEnv(&tempDirPath, &cpVIP, &clusterName, manifestValues, localIPv4, localIPv6, imagePath, configPath, k8sImagePath,
e2e.DualstackFamilyIPv6, e2e.IPv6Family, []string{e2e.IPv6Family}, &client, &gobgpPeers, v129, kubeVIPBGPManifestTemplate, &gobgpClient,
logger, nodesNumber, "auto_sourceif", "mpbgp-if-ipv6")
})
AfterAll(func() {
for _, n := range gobgpPeers {
_, err := gobgpClient.DeletePeer(context.TODO(), &api.DeletePeerRequest{
Address: n.IP,
})
Expect(err).ToNot(HaveOccurred())
}
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("advertise IPv4 routes over IPv6 session",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv4Family, api.Family_AFI_IP, []corev1.IPFamily{corev1.IPv4Protocol}, svcName, trafficPolicy, client, 1, gobgpClient, containerIP)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only stops advertising route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
testBGP(offset, e2e.IPv4Family, api.Family_AFI_IP, []corev1.IPFamily{corev1.IPv4Protocol}, svcName, trafficPolicy, client, 2, gobgpClient, containerIP)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
}
})
func testBGP(offset uint, lbFamily string, afiFamily api.Family_Afi, svcFamily []corev1.IPFamily, svcName string,
trafficPolicy corev1.ServiceExternalTrafficPolicy, client kubernetes.Interface, numberOfServices int, gobgpClient api.GobgpApiClient, expectedNexthop string) {
lbAddress := e2e.GenerateVIP(lbFamily, offset)
routeCheckFamily := &api.Family{
Afi: afiFamily,
Safi: api.Family_SAFI_UNICAST,
}
testServiceBGP(svcName, lbAddress, trafficPolicy, client, svcFamily, numberOfServices, gobgpClient, routeCheckFamily, expectedNexthop)
}
func setupEnv(tempDirPath, cpVIP, clusterName *string, manifestValues *e2e.KubevipManifestValues,
localIPv4, localIPv6, imagePath, configPath, k8sImagePath, clusterAddrFamily, bgpClientAddrFamily string, peerAddrFamily []string, client *kubernetes.Interface,
gobgpPeers *[]*e2e.BGPPeerValues, v129 bool, kubeVIPBGPManifestTemplate *template.Template, gobgpClient *api.GobgpApiClient,
logger log.Logger, nodesNumber int, mpbgpnexthop, clusterNameSuffix string) (string, string) {
var err error
*tempDirPath, err = os.MkdirTemp("", "kube-vip-test")
Expect(err).ToNot(HaveOccurred())
*cpVIP = e2e.GenerateVIP(clusterAddrFamily, SOffset.Get())
var clusterIPFamily kindconfigv1alpha4.ClusterIPFamily
var podSubnet, serviceSubnet string
switch clusterAddrFamily {
case e2e.IPv6Family:
clusterIPFamily = kindconfigv1alpha4.IPv6Family
case e2e.DualstackFamily:
clusterIPFamily = kindconfigv1alpha4.DualStackFamily
case e2e.DualstackFamilyIPv6:
clusterIPFamily = kindconfigv1alpha4.DualStackFamily
podSubnet = "fd00:10:244::/56,10.244.0.0/16"
serviceSubnet = "fd00:10:96::/112,10.96.0.0/16"
default:
clusterIPFamily = kindconfigv1alpha4.IPv4Family
}
networking := &kindconfigv1alpha4.Networking{
IPFamily: clusterIPFamily,
}
if podSubnet != "" && serviceSubnet != "" {
networking.PodSubnet = podSubnet
networking.ServiceSubnet = serviceSubnet
}
kvPeers := []*e2e.BGPPeerValues{}
if slices.Contains(peerAddrFamily, e2e.IPv4Family) {
kvPeers = append(kvPeers, &e2e.BGPPeerValues{
IP: localIPv4,
AS: goBGPAS,
IPFamily: e2e.IPv4Family,
})
}
if slices.Contains(peerAddrFamily, e2e.IPv6Family) {
kvPeers = append(kvPeers, &e2e.BGPPeerValues{
IP: localIPv6,
AS: goBGPAS,
IPFamily: e2e.IPv6Family,
})
}
kvPeersStr := []string{}
for _, p := range kvPeers {
kvPeersStr = append(kvPeersStr, p.String())
}
manifestValues = &e2e.KubevipManifestValues{
ControlPlaneVIP: *cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "false",
SvcEnable: "true",
SvcElectionEnable: "false",
BGPAS: kubevipAS,
BGPPeers: strings.Join(kvPeersStr, ","),
MPBGPNexthop: mpbgpnexthop,
MPBGPNexthopIPv4: defaultFixedNexthopv4,
MPBGPNexthopIPv6: defaultFixedNexthopv6,
}
By(manifestValues.BGPPeers)
*clusterName, *client = prepareCluster(*tempDirPath, clusterNameSuffix, k8sImagePath, v129, kubeVIPBGPManifestTemplate, logger, manifestValues, networking, nodesNumber, nil)
container := fmt.Sprintf("%s-control-plane", *clusterName)
containerIPv4, containerIPv6, err := GetContainerIPs(container)
Expect(err).ToNot(HaveOccurred())
if slices.Contains(peerAddrFamily, e2e.IPv4Family) {
*gobgpPeers = append(*gobgpPeers, &e2e.BGPPeerValues{
IP: containerIPv4,
AS: kubevipAS,
IPFamily: e2e.IPv4Family,
})
}
if slices.Contains(peerAddrFamily, e2e.IPv6Family) {
*gobgpPeers = append(*gobgpPeers, &e2e.BGPPeerValues{
IP: containerIPv6,
AS: kubevipAS,
IPFamily: e2e.IPv6Family,
})
}
if bgpClientAddrFamily == e2e.IPv6Family {
*gobgpClient, err = newGoBGPClient(localIPv6, goBGPPort)
} else {
*gobgpClient, err = newGoBGPClient(localIPv4, goBGPPort)
}
Expect(err).ToNot(HaveOccurred())
for _, p := range *gobgpPeers {
if slices.Contains(peerAddrFamily, p.IPFamily) {
peerCtx := context.TODO()
Eventually(peerCtx, func() error {
_, err = (*gobgpClient).AddPeer(context.TODO(), &api.AddPeerRequest{
Peer: &api.Peer{
Conf: &api.PeerConf{
NeighborAddress: p.IP,
PeerAsn: uint32(p.AS),
},
AfiSafis: []*api.AfiSafi{
{
Config: &api.AfiSafiConfig{
Enabled: true,
Family: &api.Family{
Afi: api.Family_AFI_IP6,
Safi: api.Family_SAFI_UNICAST,
},
},
},
{
Config: &api.AfiSafiConfig{
Enabled: true,
Family: &api.Family{
Afi: api.Family_AFI_IP,
Safi: api.Family_SAFI_UNICAST,
},
},
},
},
},
})
return err
}, "120s", "100ms").Should(Succeed())
}
}
return containerIPv4, containerIPv6
}
func testServiceBGP(svcName, lbAddress string, trafficPolicy corev1.ServiceExternalTrafficPolicy,
client kubernetes.Interface, serviceAddrFamily []corev1.IPFamily, numberOfServices int,
gobgpClient api.GobgpApiClient, gobgpFamily *api.Family, expectedNexthop string) {
lbAddresses := vip.Split(lbAddress)
services := []string{}
for i := range numberOfServices {
services = append(services, fmt.Sprintf("%s-%d", svcName, i))
}
for _, svc := range services {
createTestService(svc, dsNamespace, dsName, lbAddress,
client, corev1.IPFamilyPolicyPreferDualStack, serviceAddrFamily, trafficPolicy)
}
for _, addr := range lbAddresses {
paths := checkGoBGPPaths(context.Background(), gobgpClient, gobgpFamily, []*api.TableLookupPrefix{{Prefix: addr}}, 1)
Expect(strings.Contains(paths[0].Prefix, lbAddress)).To(BeTrue())
if expectedNexthop != "" {
Expect(strings.Contains(paths[0].String(), fmt.Sprintf("next_hop:\"%s\"", expectedNexthop)) || strings.Contains(paths[0].String(), fmt.Sprintf("next_hops:\"%s\"", expectedNexthop))).To(BeTrue())
}
}
for i := range numberOfServices {
err := client.CoreV1().Services(dsNamespace).Delete(context.TODO(), services[i], metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())
if i < numberOfServices-1 {
for _, addr := range lbAddresses {
paths := checkGoBGPPaths(context.Background(), gobgpClient, gobgpFamily, []*api.TableLookupPrefix{{Prefix: addr}}, 1)
Expect(strings.Contains(paths[0].Prefix, lbAddress)).To(BeTrue())
if expectedNexthop != "" {
Expect(strings.Contains(paths[0].String(), fmt.Sprintf("next_hop:\"%s\"", expectedNexthop)) || strings.Contains(paths[0].String(), fmt.Sprintf("next_hops:\"%s\"", expectedNexthop))).To(BeTrue())
}
}
}
}
for _, addr := range lbAddresses {
checkGoBGPPaths(context.Background(), gobgpClient, gobgpFamily, []*api.TableLookupPrefix{{Prefix: addr}}, 0)
}
}
func GetContainerIPs(containerName string) (string, string, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
panic(err)
}
containers, err := cli.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return "", "", fmt.Errorf("failed to list containers: %w", err)
}
for _, c := range containers {
for _, n := range c.Names {
if n[1:] == containerName {
fmt.Println(n)
for _, n := range c.NetworkSettings.Networks {
return n.IPAddress, n.GlobalIPv6Address, nil
}
}
}
}
return "", "", nil
}
func newGoBGPClient(address string, port uint32) (api.GobgpApiClient, error) {
grpcOpts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
target := net.JoinHostPort(address, strconv.Itoa(int(port)))
conn, err := grpc.NewClient(target, grpcOpts...)
if err != nil {
return nil, fmt.Errorf("failed to connect to GoBGP server %q: %w", target, err)
}
return api.NewGobgpApiClient(conn), nil
}
func checkGoBGPPaths(ctx context.Context, client api.GobgpApiClient, family *api.Family, prefixes []*api.TableLookupPrefix, expectedPaths int) []*api.Destination {
var paths []*api.Destination
Eventually(func() error {
var err error
paths, err = getGoBGPPaths(ctx, client, family, prefixes)
if err != nil {
return err
}
if len(paths) != expectedPaths {
return fmt.Errorf("expected %d paths, but found %d", expectedPaths, len(paths))
}
return nil
}, "120s").ShouldNot(HaveOccurred())
return paths
}
func getGoBGPPaths(ctx context.Context, client api.GobgpApiClient, family *api.Family, prefixes []*api.TableLookupPrefix) ([]*api.Destination, error) {
pathCtx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
stream, err := client.ListPath(pathCtx, &api.ListPathRequest{
TableType: api.TableType_GLOBAL,
Family: family,
Name: "",
Prefixes: prefixes,
SortType: api.ListPathRequest_PREFIX,
})
if err != nil {
return nil, err
}
rib := make([]*api.Destination, 0)
for {
r, err := stream.Recv()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
rib = append(rib, r.Destination)
}
return rib, nil
}
func startGoBGP(config string, kill chan any) {
By("starting GoBGP server")
cmd := exec.Command("../../bin/gobgpd", "-f", config)
go cmd.Run()
<-kill
By("stopping GoBGP server")
err := cmd.Process.Kill()
Expect(err).ToNot(HaveOccurred())
}

583
testing/e2e/e2e_rt_test.go Normal file
View File

@@ -0,0 +1,583 @@
//go:build e2e
// +build e2e
package e2e_test
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"text/template"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
kindconfigv1alpha4 "sigs.k8s.io/kind/pkg/apis/config/v1alpha4"
"sigs.k8s.io/kind/pkg/log"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/kube-vip/kube-vip/pkg/vip"
"github.com/kube-vip/kube-vip/testing/e2e"
"github.com/kube-vip/kube-vip/testing/services/pkg/deployment"
)
var _ = Describe("kube-vip routing table mode", func() {
if Mode == ModeRT {
var (
logger log.Logger
imagePath string
k8sImagePath string
configPath string
kubeVIPRoutingTableManifestTemplate *template.Template
tempDirPath string
v129 bool
)
BeforeEach(func() {
klog.SetOutput(GinkgoWriter)
logger = e2e.TestLogger{}
imagePath = os.Getenv("E2E_IMAGE_PATH") // Path to kube-vip image
configPath = os.Getenv("CONFIG_PATH") // path to the api server config
k8sImagePath = os.Getenv("K8S_IMAGE_PATH") // path to the kubernetes image (version for kind)
if configPath == "" {
configPath = "/etc/kubernetes/admin.conf"
}
_, v129 = os.LookupEnv("V129")
curDir, err := os.Getwd()
Expect(err).NotTo(HaveOccurred())
templateRoutingTablePath := filepath.Join(curDir, "kube-vip-routing-table.yaml.tmpl")
kubeVIPRoutingTableManifestTemplate, err = template.New("kube-vip-routing-table.yaml.tmpl").ParseFiles(templateRoutingTablePath)
Expect(err).NotTo(HaveOccurred())
tempDirPath, err = os.MkdirTemp("", "kube-vip-test")
Expect(err).NotTo(HaveOccurred())
})
Describe("kube-vip IPv4 control-plane routing table mode functionality", Ordered, func() {
var (
cpVIP string
clusterName string
nodesNumber = 3
)
BeforeAll(func() {
cpVIP = e2e.GenerateVIP(e2e.IPv4Family, SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.IPv4Family,
}
manifestValues := &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "true",
SvcEnable: "false",
SvcElectionEnable: "false",
}
clusterName, _ = prepareCluster(tempDirPath, "rt-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
It("setups IPv4 address and route on control-plane node", func() {
By(withTimestamp("sitting for a few seconds to hopefully allow kube-vip to start"))
time.Sleep(30 * time.Second)
for i := 1; i <= nodesNumber; i++ {
var container string
if i > 1 {
container = fmt.Sprintf("%s-control-plane%d", clusterName, i)
} else {
container = fmt.Sprintf("%s-control-plane", clusterName)
}
checkIPAddress(cpVIP, container, true)
e2e.CheckRoutePresence(cpVIP, container, true)
}
})
})
Describe("kube-vip IPv6 control-plane routing table mode functionality", Ordered, func() {
var (
cpVIP string
clusterName string
nodesNumber = 3
)
BeforeAll(func() {
cpVIP = e2e.GenerateVIP(e2e.IPv6Family, SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.IPv6Family,
}
manifestValues := &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "true",
SvcEnable: "false",
SvcElectionEnable: "false",
}
clusterName, _ = prepareCluster(tempDirPath, "rt-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
It("setups IPv6 address and route on control-plane node", func() {
By(withTimestamp("sitting for a few seconds to hopefully allow kube-vip to start"))
time.Sleep(30 * time.Second)
for i := 1; i <= nodesNumber; i++ {
var container string
if i > 1 {
container = fmt.Sprintf("%s-control-plane%d", clusterName, i)
} else {
container = fmt.Sprintf("%s-control-plane", clusterName)
}
checkIPAddress(cpVIP, container, true)
e2e.CheckRoutePresence(cpVIP, container, true)
}
})
})
Describe("kube-vip DualStack control-plane routing table mode functionality - IPv4 primary", Ordered, func() {
var (
cpVIP string
clusterName string
nodesNumber = 3
)
BeforeAll(func() {
cpVIP = e2e.GenerateDualStackVIP(SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.DualStackFamily,
}
manifestValues := &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "true",
SvcEnable: "false",
SvcElectionEnable: "false",
}
networkInterface := ""
if networkInterface = os.Getenv("NETWORK_INTERFACE"); networkInterface == "" {
networkInterface = "br-"
}
localIPv6, localIPv6Net, err := deployment.GetLocalIPv6(networkInterface)
Expect(err).ToNot(HaveOccurred())
addSAN := &san{
ip: localIPv6,
ipnet: localIPv6Net,
}
clusterName, _ = prepareCluster(tempDirPath, "rt-ds-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, addSAN)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
It("setups DualStack addresses and routes on control-plane nodes", func() {
By(withTimestamp("sitting for a few seconds to hopefully allow kube-vip to start"))
time.Sleep(30 * time.Second)
for i := 1; i <= nodesNumber; i++ {
var container string
if i > 1 {
container = fmt.Sprintf("%s-control-plane%d", clusterName, i)
} else {
container = fmt.Sprintf("%s-control-plane", clusterName)
}
addresses := vip.Split(cpVIP)
for _, addr := range addresses {
checkIPAddress(addr, container, true)
e2e.CheckRoutePresence(addr, container, true)
}
}
})
})
Describe("kube-vip DualStack control-plane routing table mode functionality - IPv6 primary", Ordered, func() {
var (
cpVIP string
clusterName string
nodesNumber = 3
)
BeforeAll(func() {
cpVIP = e2e.GenerateDualStackVIP(SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.DualStackFamily,
PodSubnet: "fd00:10:244::/56,10.244.0.0/16",
ServiceSubnet: "fd00:10:96::/112,10.96.0.0/16",
}
manifestValues := &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "true",
SvcEnable: "false",
SvcElectionEnable: "false",
}
networkInterface := ""
if networkInterface = os.Getenv("NETWORK_INTERFACE"); networkInterface == "" {
networkInterface = "br-"
}
localIPv4, localIPv4Net, err := deployment.GetLocalIPv4(networkInterface)
Expect(err).ToNot(HaveOccurred())
addSAN := &san{
ip: localIPv4,
ipnet: localIPv4Net,
}
clusterName, _ = prepareCluster(tempDirPath, "rt-ds-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, addSAN)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
It("setups DualStack addresses and routes on control-plane nodes", func() {
By(withTimestamp("sitting for a few seconds to hopefully allow kube-vip to start"))
time.Sleep(30 * time.Second)
for i := 1; i <= nodesNumber; i++ {
var container string
if i > 1 {
container = fmt.Sprintf("%s-control-plane%d", clusterName, i)
} else {
container = fmt.Sprintf("%s-control-plane", clusterName)
}
addresses := vip.Split(cpVIP)
for _, addr := range addresses {
checkIPAddress(addr, container, true)
e2e.CheckRoutePresence(addr, container, true)
}
}
})
})
Describe("kube-vip IPv4 services routing table mode functionality", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
svcElection bool
ipFamily []corev1.IPFamily
nodesNumber = 1
)
BeforeAll(func() {
cpVIP = e2e.GenerateVIP(e2e.IPv4Family, SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.IPv4Family,
}
manifestValues = &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "false",
SvcEnable: "true",
SvcElectionEnable: "false",
}
var err error
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
Expect(err).ToNot(HaveOccurred())
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol}
clusterName, client = prepareCluster(tempDirPath, "rt-svc-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("configures an IPv4 routes for services",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateVIP(e2e.IPv4Family, offset)
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName, trafficPolicy, client, svcElection, ipFamily, 1)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only removes route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateVIP(e2e.IPv4Family, offset)
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName, trafficPolicy, client, svcElection, ipFamily, 2)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip IPv6 services routing table mode functionality", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
svcElection bool
ipFamily []corev1.IPFamily
nodesNumber = 1
)
BeforeAll(func() {
cpVIP = e2e.GenerateVIP(e2e.IPv6Family, SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.IPv6Family,
}
manifestValues = &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "false",
SvcEnable: "true",
SvcElectionEnable: "false",
}
var err error
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
Expect(err).ToNot(HaveOccurred())
ipFamily = []corev1.IPFamily{corev1.IPv6Protocol}
clusterName, client = prepareCluster(tempDirPath, "rt-svc-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("configures an IPv6 routes for services",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateVIP(e2e.IPv6Family, offset)
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName, trafficPolicy, client, svcElection, ipFamily, 1)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only removes route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateVIP(e2e.IPv6Family, offset)
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName, trafficPolicy, client, svcElection, ipFamily, 2)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip DualStack services routing table mode functionality - IPv4 primary", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
svcElection bool
ipFamily []corev1.IPFamily
nodesNumber = 1
)
BeforeAll(func() {
cpVIP = e2e.GenerateDualStackVIP(SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.DualStackFamily,
}
manifestValues = &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "false",
SvcEnable: "true",
SvcElectionEnable: "false",
EnableEndpointslices: "true",
}
var err error
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
Expect(err).ToNot(HaveOccurred())
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
clusterName, client = prepareCluster(tempDirPath, "rt-ds-svc-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("configures an IPv4 and IPv6 routes for services",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateDualStackVIP(offset)
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName, trafficPolicy, client, svcElection, ipFamily, 1)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only removes route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateDualStackVIP(offset)
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName, trafficPolicy, client, svcElection, ipFamily, 2)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
Describe("kube-vip DualStack services routing table mode functionality - IPv6 primary", Ordered, func() {
var (
cpVIP string
clusterName string
client kubernetes.Interface
manifestValues *e2e.KubevipManifestValues
svcElection bool
ipFamily []corev1.IPFamily
nodesNumber = 1
)
BeforeAll(func() {
cpVIP = e2e.GenerateDualStackVIP(SOffset.Get())
networking := &kindconfigv1alpha4.Networking{
IPFamily: kindconfigv1alpha4.DualStackFamily,
PodSubnet: "fd00:10:244::/56,10.244.0.0/16",
ServiceSubnet: "fd00:10:96::/112,10.96.0.0/16",
}
manifestValues = &e2e.KubevipManifestValues{
ControlPlaneVIP: cpVIP,
ImagePath: imagePath,
ConfigPath: configPath,
ControlPlaneEnable: "false",
SvcEnable: "true",
SvcElectionEnable: "false",
EnableEndpointslices: "true",
}
var err error
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
Expect(err).ToNot(HaveOccurred())
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
clusterName, client = prepareCluster(tempDirPath, "rt-ds-svc-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil)
})
AfterAll(func() {
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
})
DescribeTable("configures an IPv4 and IPv6 routes for services",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateDualStackVIP(offset)
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName, trafficPolicy, client, svcElection, ipFamily, 1)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
DescribeTable("only removes route if it was referenced by multiple services and all of them were deleted",
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
lbAddress := e2e.GenerateDualStackVIP(offset)
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName, trafficPolicy, client, svcElection, ipFamily, 2)
},
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
Entry("with external traffic policy - local", "test-svc-local", SOffset.Get(), corev1.ServiceExternalTrafficPolicyLocal),
)
})
}
})
func testServiceRT(svcName, lbAddress, leaseName, leaseNamespace, clusterName string, trafficPolicy corev1.ServiceExternalTrafficPolicy,
client kubernetes.Interface, serviceElection bool, ipFamily []corev1.IPFamily, numberOfServices int) {
lbAddresses := vip.Split(lbAddress)
services := []string{}
for i := range numberOfServices {
services = append(services, fmt.Sprintf("%s-%d", svcName, i))
}
for _, svc := range services {
createTestService(svc, dsNamespace, dsName, lbAddress,
client, corev1.IPFamilyPolicyPreferDualStack, ipFamily, trafficPolicy)
}
var container string
if serviceElection {
container = e2e.GetLeaseHolder(leaseName, leaseNamespace, client)
} else {
container = fmt.Sprintf("%s-control-plane", clusterName)
}
for _, addr := range lbAddresses {
e2e.CheckRoutePresence(addr, container, true)
}
for i := range numberOfServices {
expected := i < numberOfServices-1
err := client.CoreV1().Services(dsNamespace).Delete(context.TODO(), services[i], metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())
for _, addr := range lbAddresses {
e2e.CheckRoutePresence(addr, container, expected)
}
}
}
type san struct {
ip *net.IP
ipnet *net.IPNet
}

View File

@@ -4,15 +4,41 @@
package e2e_test
import (
"log"
"os"
"sync"
"testing"
"github.com/kube-vip/kube-vip/testing/e2e"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
"github.com/kube-vip/kube-vip/testing/e2e"
const (
ModeEnv = "TEST_MODE"
ModeARP = "arp"
ModeRT = "rt"
ModeBGP = "bgp"
)
var (
SOffset *e2e.SecureOffset
ConfigMtx *sync.Mutex
Mode string
)
func TestE2E(t *testing.T) {
mode := os.Getenv(ModeEnv)
if mode == "" {
Mode = ModeARP
} else if mode != ModeARP && mode != ModeRT && mode != ModeBGP {
log.Fatal("invalid", "mode", mode)
os.Exit(1)
} else {
Mode = mode
}
SOffset = e2e.NewOffset(5)
ConfigMtx = &sync.Mutex{}
RegisterFailHandler(Fail)
RunSpecs(t, "E2E Suite")
}

Some files were not shown because too many files have changed in this diff Show More