mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/kube-vip/kube-vip.git
synced 2026-09-21 00:23:59 +08:00
Compare commits
154 Commits
v0.9.2
...
error_warn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83d2092fec | ||
|
|
6c9c5af373 | ||
|
|
509eeea1d4 | ||
|
|
c00a61f45e | ||
|
|
0ea24655eb | ||
|
|
288cd9a8b0 | ||
|
|
2c00d2bc05 | ||
|
|
5cf899c88c | ||
|
|
16fa1bcc26 | ||
|
|
a67ef25c15 | ||
|
|
c82738633c | ||
|
|
11e419595b | ||
|
|
1db99a10dc | ||
|
|
f51f3276b5 | ||
|
|
199bc43c5c | ||
|
|
16afc9c1d4 | ||
|
|
76156b3f3b | ||
|
|
5a1e8c1a3f | ||
|
|
2d0f0734c4 | ||
|
|
42b97175e3 | ||
|
|
eec091af23 | ||
|
|
6f4f870800 | ||
|
|
3a5a59ae64 | ||
|
|
e8484fa1f3 | ||
|
|
55ccb8cd87 | ||
|
|
09edf341ab | ||
|
|
8380e4f07e | ||
|
|
4a07466467 | ||
|
|
cbdc86ac8f | ||
|
|
7ea39fa7b5 | ||
|
|
7eed2a33dc | ||
|
|
95bb7b9a85 | ||
|
|
2762fb624c | ||
|
|
3924a57168 | ||
|
|
8750b3331c | ||
|
|
be9415fef1 | ||
|
|
df13a69e26 | ||
|
|
6b60780d6c | ||
|
|
9786aa9446 | ||
|
|
71ca2614d2 | ||
|
|
3d171a937e | ||
|
|
2663a1b222 | ||
|
|
51ebcc40f0 | ||
|
|
95c45b0b32 | ||
|
|
a3c5be3242 | ||
|
|
c5c920f341 | ||
|
|
2e2951b35b | ||
|
|
4b741e767a | ||
|
|
66adbd4abb | ||
|
|
c9e4e7aea1 | ||
|
|
56a441f700 | ||
|
|
f9951bac77 | ||
|
|
d7a66ce20f | ||
|
|
73d9ce7f44 | ||
|
|
14ff1b9fec | ||
|
|
9a9c5998d8 | ||
|
|
edb9dcb626 | ||
|
|
89559b97af | ||
|
|
cc1d9ac16d | ||
|
|
1d9454c61b | ||
|
|
8409073e7a | ||
|
|
6d419f32bc | ||
|
|
9d68054e9a | ||
|
|
5dcfb8742f | ||
|
|
7e6f70b027 | ||
|
|
3e10aa85d0 | ||
|
|
16b9f6767e | ||
|
|
d8a9727ff5 | ||
|
|
016a899e60 | ||
|
|
68b39a83e0 | ||
|
|
93dabff000 | ||
|
|
630be48010 | ||
|
|
ea78d291ce | ||
|
|
2074be2939 | ||
|
|
7946c17c00 | ||
|
|
a0295d6a2f | ||
|
|
d70068b1a0 | ||
|
|
89baba07f5 | ||
|
|
95995500bc | ||
|
|
b1183e8a93 | ||
|
|
101f722110 | ||
|
|
3f390120c1 | ||
|
|
7baa8a3141 | ||
|
|
6435581674 | ||
|
|
7eb730c0ef | ||
|
|
f6a7aeb130 | ||
|
|
29296a9dc2 | ||
|
|
4d4a2f0ee1 | ||
|
|
e4e398bfcf | ||
|
|
8bd2c26a8f | ||
|
|
bd8f30d67d | ||
|
|
a3a429b2b9 | ||
|
|
4f7ce8a1c8 | ||
|
|
d2ecf22edd | ||
|
|
3963172e49 | ||
|
|
80c6b0bde4 | ||
|
|
4f59df38f3 | ||
|
|
1a3e6c9d5f | ||
|
|
0635ec9e01 | ||
|
|
3fff60a64a | ||
|
|
f05f0469cc | ||
|
|
00337a756b | ||
|
|
d3473b5d68 | ||
|
|
889d442288 | ||
|
|
b2c04c9058 | ||
|
|
0889ebed7d | ||
|
|
d1430e79e2 | ||
|
|
31eca367ab | ||
|
|
bdd353d0fd | ||
|
|
4deb0592f6 | ||
|
|
d8877072d4 | ||
|
|
89a8dc7de1 | ||
|
|
704c346f5e | ||
|
|
65061c5cd9 | ||
|
|
32233918b4 | ||
|
|
76169da60f | ||
|
|
9bcf1413f0 | ||
|
|
8e428e875f | ||
|
|
2fbecc25e5 | ||
|
|
02e77271d0 | ||
|
|
3d61888e58 | ||
|
|
efe75f491b | ||
|
|
000c139004 | ||
|
|
c39b84f0a9 | ||
|
|
ee958addaa | ||
|
|
8fe53351f8 | ||
|
|
bc9d860d83 | ||
|
|
332a23e543 | ||
|
|
b20713b50f | ||
|
|
94e96581ef | ||
|
|
61be6d0b6a | ||
|
|
be22805a7d | ||
|
|
25f6253286 | ||
|
|
f3e9fb6ea9 | ||
|
|
0f3dda02c4 | ||
|
|
a2873b5465 | ||
|
|
202d45e5ab | ||
|
|
f5e4612c03 | ||
|
|
de888c501c | ||
|
|
47bc83c248 | ||
|
|
1cf637c569 | ||
|
|
ae2571e241 | ||
|
|
86f5e9b8b2 | ||
|
|
ce61ff085a | ||
|
|
b816e154cf | ||
|
|
ac1238c337 | ||
|
|
10dbf2c0ef | ||
|
|
cf68f8639c | ||
|
|
b114c11b0f | ||
|
|
42b7a8152b | ||
|
|
f7821c7fb3 | ||
|
|
42478905d0 | ||
|
|
8d55bd3b63 | ||
|
|
16247fc3a3 |
4
.github/workflows/anchore-syft.yml
vendored
4
.github/workflows/anchore-syft.yml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
- name: Anchore SBOM Action
|
||||
uses: anchore/sbom-action@v0.20.1
|
||||
uses: anchore/sbom-action@v0.20.10
|
||||
with:
|
||||
format: cyclonedx-json
|
||||
|
||||
71
.github/workflows/ci-pull-request.yaml
vendored
Normal file
71
.github/workflows/ci-pull-request.yaml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: For each PR
|
||||
on:
|
||||
pull_request:
|
||||
jobs:
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E tests
|
||||
strategy:
|
||||
matrix:
|
||||
mode: ["arp", "rt", "bgp"]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d-%H-%M')"
|
||||
- name: Ensure fs wont cause issues
|
||||
run: sudo sysctl fs.inotify.max_user_instances=8192 && sudo sysctl fs.inotify.max_user_watches=524288
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build image locally
|
||||
run: make dockerx86Local
|
||||
- name: Run Manifest generation tests
|
||||
run: make manifest-test
|
||||
- name: Run ARP mode tests v1.29.0 onwards
|
||||
run: E2E_KEEP_LOGS=true make e2e-tests129-arp
|
||||
if: matrix.mode== 'arp'
|
||||
- name: Run RT mode tests v1.29.0 onwards
|
||||
run: E2E_KEEP_LOGS=true make e2e-tests129-rt
|
||||
if: matrix.mode== 'rt'
|
||||
- name: Get GoBGP binaries
|
||||
run: make get-gobgp
|
||||
if: matrix.mode== 'bgp'
|
||||
- name: Run BGP mode tests v1.29.0 onwards
|
||||
run: sudo -E PATH=$PATH DOCKER_API_VERSION=1.48 E2E_KEEP_LOGS=true make e2e-tests129-bgp
|
||||
if: matrix.mode== 'bgp'
|
||||
- name: Change log directory permissions
|
||||
run: sudo chmod -R 755 /tmp/kube-vip-test-${{ matrix.mode }}*
|
||||
if: matrix.mode== 'bgp' && always()
|
||||
- name: Save logs
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: e2e-test-logs-${{ matrix.mode }}-${{ steps.date.outputs.date }}
|
||||
path: /tmp/kube-vip-test-${{ matrix.mode }}*
|
||||
if: always()
|
||||
service-e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E service tests
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d-%H-%M')"
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Build image with iptables
|
||||
run: make dockerx86ActionIPTables
|
||||
- name: Run tests
|
||||
run: DOCKERTAG=action E2E_KEEP_LOGS=true make service-tests
|
||||
- name: Save logs
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: services-test-logs-${{ steps.date.outputs.date }}
|
||||
path: /tmp/kube-vip-service-tests*
|
||||
if: always()
|
||||
55
.github/workflows/ci.yaml
vendored
55
.github/workflows/ci.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: For each commit and PR
|
||||
name: For each commit
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
- 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.64.8
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: All checks
|
||||
@@ -24,9 +24,9 @@ jobs:
|
||||
name: Unit tests
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Run tests
|
||||
@@ -36,58 +36,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Run tests
|
||||
run: make integration-tests
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E tests
|
||||
strategy:
|
||||
matrix:
|
||||
mode: ["arp", "rt"]
|
||||
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'
|
||||
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
|
||||
image-vul-check:
|
||||
runs-on: ubuntu-latest
|
||||
name: Image vulnerability scan
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Build image with iptables
|
||||
run: make dockerx86ActionIPTables
|
||||
- name: Run Trivy vulnerability scanner
|
||||
|
||||
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
@@ -38,16 +38,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
2
.github/workflows/main.yaml
vendored
2
.github/workflows/main.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Generate Metadata
|
||||
uses: docker/metadata-action@v5.7.0
|
||||
uses: docker/metadata-action@v5.9.0
|
||||
id: metadata
|
||||
with:
|
||||
labels: |
|
||||
|
||||
33
CHANGELOG.md
Normal file
33
CHANGELOG.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- SIGUSR1 signal handler for runtime configuration dumps (#1301)
|
||||
- Send SIGUSR1 to kube-vip process to dump current configuration to stdout
|
||||
- Configuration dump includes:
|
||||
- Basic configuration (VIP, interface, port, namespace settings)
|
||||
- BGP configuration (enabled status, AS number, router ID, peers)
|
||||
- ARP/NDP configuration (enabled status, broadcast rate)
|
||||
- Services configuration (enabled status, load balancer settings)
|
||||
- Network interfaces status
|
||||
- Leader election configuration (type, lease details)
|
||||
- Runtime statistics (load balancer, Prometheus, health check settings)
|
||||
- Output format: Human-readable plaintext via fmt.Printf()
|
||||
- Thread-safe implementation using mutex protection
|
||||
- Non-disruptive: Process continues running after configuration dump
|
||||
- Added comprehensive unit tests for all dump methods
|
||||
- Added E2E tests for signal handling
|
||||
|
||||
### Changed
|
||||
- Updated signal handlers in manager_arp.go, manager_bgp.go, manager_wireguard.go, and manager_table.go to use switch statement pattern for handling multiple signals (SIGUSR1, SIGINT, SIGTERM)
|
||||
|
||||
## [v1.0.1] - Previous Release
|
||||
|
||||
### Previous changes
|
||||
- See git history for changes prior to CHANGELOG.md introduction
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:experimental
|
||||
|
||||
FROM golang:1.24.4-alpine3.22 as dev
|
||||
FROM golang:1.25.4-alpine3.22 as dev
|
||||
RUN apk add --no-cache git ca-certificates make
|
||||
RUN adduser -D appuser
|
||||
COPY . /src/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:experimental
|
||||
|
||||
FROM golang:1.24.4-alpine3.22 as dev
|
||||
FROM golang:1.25.4-alpine3.22 as dev
|
||||
RUN apk add --no-cache git make
|
||||
RUN adduser -D appuser
|
||||
COPY . /src/
|
||||
@@ -11,7 +11,7 @@ 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.22.0
|
||||
FROM alpine:3.22.2
|
||||
# Update pkgs and add iptables
|
||||
RUN apk upgrade && \
|
||||
apk add --no-cache iptables iptables-legacy
|
||||
|
||||
45
Makefile
45
Makefile
@@ -5,7 +5,7 @@ TARGET := kube-vip
|
||||
.DEFAULT_GOAL := $(TARGET)
|
||||
|
||||
# These will be provided to the target
|
||||
VERSION := v0.9.2
|
||||
VERSION := v1.0.2
|
||||
|
||||
BUILD := `git rev-parse HEAD`
|
||||
|
||||
@@ -15,7 +15,7 @@ TARGETOS=linux
|
||||
# Use linker flags to provide version/build settings to the target
|
||||
LDFLAGS=-ldflags "-s -w -X=main.Version=$(VERSION) -X=main.Build=$(BUILD) -extldflags -static"
|
||||
DOCKERTAG ?= $(VERSION)
|
||||
REPOSITORY ?= plndr
|
||||
REPOSITORY ?= docker.io/plndr
|
||||
|
||||
.PHONY: all build clean install uninstall simplify check run e2e-tests
|
||||
|
||||
@@ -109,22 +109,22 @@ run: install
|
||||
manifests:
|
||||
@make build
|
||||
@mkdir -p ./docs/manifests/$(VERSION)/
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-arp.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-lb.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --bgp --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-bgp.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-arp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --arp --leaderElection --controlplane --services --inCluster --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-ds-lb.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --bgp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-bgp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --bgp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-bgp-em-ds.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-arp.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-lb.yaml
|
||||
@./kube-vip manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --controlplane --services > ./docs/manifests/$(VERSION)/kube-vip-bgp.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-arp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster --enableLoadBalancer > ./docs/manifests/$(VERSION)/kube-vip-arp-ds-lb.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-bgp-ds.yaml
|
||||
@./kube-vip manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --leaderElection --controlplane --services --inCluster > ./docs/manifests/$(VERSION)/kube-vip-bgp-em-ds.yaml
|
||||
@-rm ./kube-vip
|
||||
|
||||
manifest-test:
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --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
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --enableLoadBalancer
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest pod --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --controlplane --services
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --leaderElection --controlplane --services --inCluster --enableLoadBalancer
|
||||
docker run $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --interface eth0 --vip 192.168.0.1 --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --bgp --leaderElection --controlplane --services --inCluster
|
||||
|
||||
unit-tests:
|
||||
go test ./...
|
||||
@@ -141,7 +141,10 @@ e2e-tests129-arp:
|
||||
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: e2e-tests129-arp e2e-tests129-rt
|
||||
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/services -Services -simple -deployments -leaderActive -leaderFailover -localDeploy -egress -egressIPv6 -dualStack
|
||||
@@ -163,8 +166,14 @@ kind-quick:
|
||||
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 -
|
||||
docker run --network host --rm $(REPOSITORY)/$(TARGET):$(DOCKERTAG) manifest daemonset --services --inCluster --image "$(REPOSITORY)/$(TARGET):$(DOCKERTAG)" --arp --servicesElection --interface eth0 | kubectl apply -f -
|
||||
|
||||
kind-reload:
|
||||
kind load docker-image $(REPOSITORY)/$(TARGET):$(DOCKERTAG) --name kube-vip
|
||||
kind load docker-image $(REPOSITORY)/$(TARGET):$(DOCKERTAG) --name services
|
||||
kubectl rollout restart -n kube-system daemonset/kube-vip-ds
|
||||
|
||||
get-gobgp:
|
||||
mkdir -p bin
|
||||
wget -nc --directory-prefix=bin https://github.com/osrg/gobgp/releases/download/v3.37.0/gobgp_3.37.0_linux_amd64.tar.gz
|
||||
tar -xvzf bin/gobgp_3.37.0_linux_amd64.tar.gz -C bin
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ High Availability and Load-Balancing
|
||||
|
||||

|
||||
|
||||
[](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml)
|
||||
[](https://github.com/kube-vip/kube-vip/actions/workflows/main.yaml) [](https://insights.linuxfoundation.org/project/kube-vip) [&message=212&color=0094FF&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/kube-vip)
|
||||
|
||||
## Overview
|
||||
Kubernetes Virtual IP and Load-Balancer for both control plane and Kubernetes services
|
||||
|
||||
@@ -58,14 +58,14 @@ var kubeKubeadmInit = &cobra.Command{
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating VIPSubnet", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, image, Release.Version, inCluster)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
@@ -103,14 +103,14 @@ var kubeKubeadmJoin = &cobra.Command{
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating VIPSubnet", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, image, Release.Version, inCluster)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ var taint, role, rolebinding bool
|
||||
|
||||
func init() {
|
||||
kubeManifest.PersistentFlags().BoolVar(&inCluster, "inCluster", false, "Use the incluster token to authenticate to Kubernetes")
|
||||
kubeManifest.PersistentFlags().StringVar(&image, "image", "ghcr.io/kube-vip/kube-vip", "Define a hardcoded image with or without tag for the manifest")
|
||||
kubeManifestDaemon.PersistentFlags().BoolVar(&taint, "taint", false, "Taint the manifest for only running on control planes")
|
||||
kubeManifestRbac.PersistentFlags().BoolVar(&role, "role", false, "Generate only a Role inside the serviceNamespace access")
|
||||
kubeManifestRbac.PersistentFlags().BoolVar(&rolebinding, "rolebinding", false, "Generate only a RoleBinding for namespaced access")
|
||||
@@ -59,14 +60,14 @@ var kubeManifestPod = &cobra.Command{
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("config parse", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, Release.Version, inCluster)
|
||||
cfg := kubevip.GeneratePodManifestFromConfig(&initConfig, image, Release.Version, inCluster)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
@@ -92,14 +93,14 @@ var kubeManifestDaemon = &cobra.Command{
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("config parse", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := kubevip.GenerateDaemonsetManifestFromConfig(&initConfig, Release.Version, inCluster, taint)
|
||||
cfg := kubevip.GenerateDaemonsetManifestFromConfig(&initConfig, image, Release.Version, inCluster, taint)
|
||||
fmt.Println(cfg) // output manifest to stdout
|
||||
},
|
||||
}
|
||||
@@ -126,7 +127,7 @@ var kubeManifestRbac = &cobra.Command{
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating VIPSubnet", "err", err)
|
||||
return
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -19,9 +20,13 @@ import (
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/manager"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
// Is an option to set the image
|
||||
var image string
|
||||
|
||||
// Is kube-vip running within cluster
|
||||
var inCluster bool
|
||||
|
||||
@@ -62,6 +67,7 @@ func init() {
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableARP, "arp", false, "Enable Arp for VIP changes")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableWireguard, "wireguard", false, "Enable Wireguard for services VIPs")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableRoutingTable, "table", false, "Enable Routing Table for services VIPs")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.PreserveVIPOnLeadershipLoss, "preserveVipOnLeadershipLoss", false, "Preserve ARP VIP addresses on interface when leadership is lost (default: false for backward compatibility)")
|
||||
|
||||
// LoadBalancer flags
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableLoadBalancer, "enableLoadBalancer", false, "enable loadbalancing on the VIP with IPVS")
|
||||
@@ -93,6 +99,11 @@ 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")
|
||||
|
||||
@@ -115,15 +126,15 @@ func init() {
|
||||
|
||||
// Extended behaviour flags
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableServicesElection, "servicesElection", false, "Enable leader election per kubernetes service")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoadBalancerClassOnly, "lbClassOnly", false, "Enable load balancing only for services with LoadBalancerClass \"kube-vip.io/kube-vip-class\"")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.LoadBalancerClassName, "lbClassName", "kube-vip.io/kube-vip-class", "Name of load balancer class for kube-VIP, defaults to \"kube-vip.io/kube-vip-class\"")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoadBalancerClassOnly, "lbClassOnly", false, fmt.Sprintf("Enable load balancing only for services with LoadBalancerClass %q", kubevip.LBClassName))
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.LoadBalancerClassName, "lbClassName", kubevip.LBClassName, fmt.Sprintf("Name of load balancer class for kube-VIP, defaults to %q", kubevip.LBClassName))
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoadBalancerClassLegacyHandling, "lbClassNameLegacyHandling", true, "Use legacy LoadBalancer class name handling (e.g. accepting services both with empty and non-empty class)")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableServiceSecurity, "onlyAllowTrafficServicePorts", false, "Only allow traffic to service ports, others will be dropped, defaults to false")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableNodeLabeling, "enableNodeLabeling", false, "Enable leader node labeling with \"kube-vip.io/has-ip=<VIP address>\", defaults to false")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableNodeLabeling, "enableNodeLabeling", false, fmt.Sprintf("Enable leader node labeling with %q, defaults to false", kubevip.HasIP))
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.ServicesLeaseName, "servicesLeaseName", "plndr-svcs-lock", "Name of the lease that is used for leader election for services (in arp mode)")
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.DNSMode, "dnsMode", "first", "Name of the mode that DNS lookup will be performed (first, ipv4, ipv6, dual)")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.DisableServiceUpdates, "disableServiceUpdates", false, "If true, kube-vip will process services as usual, but will not update service's Status.LoadBalancer.Ingress slice")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableEndpointSlices, "enableEndpointSlices", false, "If enabled, kube-vip will only advertise services, but will use EndpointSlices instead of endpoints to get IPs of Pods")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.EnableEndpoints, "enableEndpoints", false, "If enabled, kube-vip will only advertise services, but will use the (deprecated since v1.33) endpoints for IP addresses")
|
||||
kubeVipCmd.PersistentFlags().BoolVar(&initConfig.LoInterfaceGlobalScope, "loInterfaceGlobalScope", false, "If true, kube-vip will set global scope when using the lo interface, otherwise a host scope will be used by default")
|
||||
kubeVipCmd.PersistentFlags().IntVar(&initConfig.HealthCheckPort, "healthCheckPort", 0, "If set to non-zero (> 1024), then this is the port that the healthcheck will listen on")
|
||||
|
||||
@@ -139,6 +150,9 @@ func init() {
|
||||
// Kubernetes client specific flags
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.K8sConfigFile, "k8sConfigPath", "/etc/kubernetes/admin.conf", "Path to the configuration file used with the Kubernetes client")
|
||||
|
||||
// Configuration file flag
|
||||
kubeVipCmd.PersistentFlags().StringVar(&initConfig.ConfigFile, "config-file", "", "Path to a JSON/YAML configuration file to load settings from")
|
||||
|
||||
kubeVipCmd.AddCommand(kubeKubeadm)
|
||||
kubeVipCmd.AddCommand(kubeManifest)
|
||||
kubeVipCmd.AddCommand(kubeVipManager)
|
||||
@@ -181,7 +195,16 @@ var kubeVipService = &cobra.Command{
|
||||
Short: "Start the Virtual IP / Load balancer as a service within a Kubernetes cluster",
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
|
||||
// parse environment variables, these will overwrite anything loaded or flags
|
||||
// Load configuration from file if specified (lowest priority)
|
||||
if initConfig.ConfigFile != "" {
|
||||
err := kubevip.MergeConfigFromFile(&initConfig, initConfig.ConfigFile)
|
||||
if err != nil {
|
||||
log.Error("loading config file", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parse environment variables, these will overwrite anything loaded from config file
|
||||
err := kubevip.ParseEnvironment(&initConfig)
|
||||
if err != nil {
|
||||
log.Error("parsing env", "err", err)
|
||||
@@ -209,7 +232,7 @@ var kubeVipService = &cobra.Command{
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("generating CIDR", "err", err)
|
||||
return
|
||||
@@ -236,7 +259,16 @@ var kubeVipManager = &cobra.Command{
|
||||
Use: "manager",
|
||||
Short: "Start the kube-vip manager",
|
||||
Run: func(cmd *cobra.Command, args []string) { //nolint TODO
|
||||
// parse environment variables, these will overwrite anything loaded or flags
|
||||
// Load configuration from file if specified (lowest priority)
|
||||
if initConfig.ConfigFile != "" {
|
||||
err := kubevip.MergeConfigFromFile(&initConfig, initConfig.ConfigFile)
|
||||
if err != nil {
|
||||
log.Error("loading config file", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parse environment variables, these will overwrite anything loaded from config file
|
||||
err := kubevip.ParseEnvironment(&initConfig)
|
||||
if err != nil {
|
||||
log.Error("parsing environment", "err", err)
|
||||
@@ -253,7 +285,7 @@ var kubeVipManager = &cobra.Command{
|
||||
|
||||
// Ensure there is an address to generate the CIDR from
|
||||
if initConfig.VIPSubnet == "" && initConfig.Address != "" {
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address)
|
||||
initConfig.VIPSubnet, err = GenerateCidrRange(initConfig.Address, initConfig.DNSMode)
|
||||
if err != nil {
|
||||
log.Error("No interface is specified for kube-vip to bind to")
|
||||
return
|
||||
@@ -443,26 +475,37 @@ func servePrometheusHTTPServer(ctx context.Context, config PrometheusHTTPServerC
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateCidrRange(address string) (string, error) {
|
||||
func GenerateCidrRange(address string, dnsMode string) (string, error) {
|
||||
var cidrs []string
|
||||
|
||||
addresses := strings.Split(address, ",")
|
||||
for _, a := range addresses {
|
||||
ip := net.ParseIP(a)
|
||||
if ip == nil {
|
||||
ips, err := net.LookupIP(a)
|
||||
// we probably are a DNS name
|
||||
ips, err := utils.LookupHost(a, dnsMode)
|
||||
if len(ips) == 0 || err != nil {
|
||||
return "", fmt.Errorf("invalid IP address: %s from [%s], %v", a, address, err)
|
||||
}
|
||||
ip = ips[0]
|
||||
}
|
||||
|
||||
if ip.To4() != nil {
|
||||
cidrs = append(cidrs, "32")
|
||||
for _, addr := range ips {
|
||||
ip = net.ParseIP(addr)
|
||||
if ip.To4() != nil {
|
||||
cidrs = append(cidrs, "32")
|
||||
} else {
|
||||
cidrs = append(cidrs, "128")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cidrs = append(cidrs, "128")
|
||||
if ip.To4() != nil {
|
||||
cidrs = append(cidrs, "32")
|
||||
} else {
|
||||
cidrs = append(cidrs, "128")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compact as DNS could have a lot of addresses
|
||||
slices.Sort(cidrs)
|
||||
cidrs = slices.Compact(cidrs)
|
||||
slices.Reverse(cidrs)
|
||||
return strings.Join(cidrs, ","), nil
|
||||
}
|
||||
|
||||
126
go.mod
126
go.mod
@@ -5,70 +5,81 @@ go 1.24.4
|
||||
require (
|
||||
github.com/cloudflare/ipvs v0.11.0
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/florianl/go-conntrack v0.4.0
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gookit/slog v0.5.8
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/google/nftables v0.3.0
|
||||
github.com/gookit/slog v0.6.0
|
||||
github.com/huin/goupnp v1.3.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d
|
||||
github.com/jpillora/backoff v1.0.0
|
||||
github.com/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.36.0
|
||||
github.com/onsi/ginkgo/v2 v2.27.2
|
||||
github.com/onsi/gomega v1.38.2
|
||||
github.com/osrg/gobgp/v3 v3.37.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
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
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.etcd.io/etcd/api/v3 v3.6.6
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6
|
||||
go.etcd.io/etcd/client/v3 v3.6.6
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329
|
||||
golang.org/x/sync v0.13.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
google.golang.org/protobuf v1.36.6
|
||||
k8s.io/api v0.32.3
|
||||
k8s.io/apimachinery v0.32.3
|
||||
k8s.io/client-go v0.32.3
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
k8s.io/api v0.34.2
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
sigs.k8s.io/kind v0.27.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
sigs.k8s.io/kind v0.30.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
al.essio.dev/pkg/shellescape v1.5.1 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-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.12.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962 // 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.26.3 // indirect
|
||||
github.com/gookit/color v1.6.0 // indirect
|
||||
github.com/gookit/goutil v0.7.1 // indirect
|
||||
github.com/gookit/gsr v0.1.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
@@ -79,28 +90,32 @@ require (
|
||||
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.5.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pborman/uuid v1.2.1 // 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.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/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tj/go-spin v1.1.0 // indirect
|
||||
@@ -111,27 +126,33 @@ require (
|
||||
github.com/xlab/c-for-go v1.3.0 // indirect
|
||||
github.com/xlab/pkgconfig v0.0.0-20170226114623-cea12a0fd245 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/grpc v1.71.1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||
modernc.org/cc/v4 v4.24.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/opt v0.1.4 // indirect
|
||||
@@ -139,5 +160,6 @@ require (
|
||||
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/structured-merge-diff/v4 v4.5.0 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
)
|
||||
|
||||
313
go.sum
313
go.sum
@@ -1,17 +1,31 @@
|
||||
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
|
||||
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
|
||||
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
|
||||
github.com/cloudflare/ipvs v0.11.0 h1:niLcbqfv8+RSYk+yI+jhiCIHobEdEhO++mcwCX0Znsw=
|
||||
github.com/cloudflare/ipvs v0.11.0/go.mod h1:XsbuKcQpqb3rpjYPtsFsM8BNZlgLw+Z0iC6RhocqDd8=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
@@ -23,18 +37,28 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k=
|
||||
github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
|
||||
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/florianl/go-conntrack v0.4.0 h1:TlYkxytdwgVayfU0cKwkHurQA0Rd1ZSEBRckRYDUu18=
|
||||
github.com/florianl/go-conntrack v0.4.0/go.mod h1:iPDx4oIats2T7X7Jm3PFyRCJM1GfZhJaSHOWROYOrE8=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
@@ -42,10 +66,17 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
|
||||
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
|
||||
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
@@ -58,13 +89,15 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -72,31 +105,31 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
|
||||
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
|
||||
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962 h1:+9C/TgFfcCmZBV7Fjb3kQCGlkpFrhtvFDgbdQHB9RaA=
|
||||
github.com/google/safetext v0.0.0-20240722112252-5a72de7e7962/go.mod h1:H3K1Iu/utuCfa10JO+GsmKUYSWi7ug57Rk6GaDRHaaQ=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
|
||||
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
|
||||
github.com/gookit/gsr v0.1.0 h1:0gadWaYGU4phMs0bma38t+Do5OZowRMEVlHv31p0Zig=
|
||||
github.com/gookit/gsr v0.1.0/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI=
|
||||
github.com/gookit/slog v0.5.8 h1:XZCeHLQvvOZWcSUDZcqxXITsL9+d1ESsKZoASBmK1lI=
|
||||
github.com/gookit/slog v0.5.8/go.mod h1:s0ViFOY/IgUuT4MDPF0l9x5/npcciy8pL4xwWZadnoc=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
||||
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/goutil v0.7.1 h1:AaFJPN9mrdeYBv8HOybri26EHGCC34WJVT7jUStGJsI=
|
||||
github.com/gookit/goutil v0.7.1/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/gsr v0.1.1 h1:TaHD3M7qa6lcAf9D2J4mGNg+QjgDtD1bw7uctF8RXOM=
|
||||
github.com/gookit/gsr v0.1.1/go.mod h1:7wv4Y4WCnil8+DlDYHBjidzrEzfHhXEoFjEA0pPPWpI=
|
||||
github.com/gookit/slog v0.6.0 h1:KEQxOJxbTtk7oyqah6nJOEKjOdI0z5qoqkX7I6G65g4=
|
||||
github.com/gookit/slog v0.6.0/go.mod h1:hPlpNi/WIcGmkEjHzQTS7s5JZkHmmnGy9sYo6csa08s=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
|
||||
@@ -112,6 +145,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
@@ -144,6 +179,8 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||
@@ -164,8 +201,8 @@ github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuri
|
||||
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
|
||||
github.com/mdlayher/netlink v1.5.0/go.mod h1:1Kr8BBFxGyUyNmztC9WLOayqYVAd2wsgOZm18nqGuzQ=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
|
||||
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
|
||||
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc=
|
||||
@@ -173,27 +210,42 @@ github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267/go.mod h1:nFZ1EtZY
|
||||
github.com/mdlayher/socket v0.1.0/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/osrg/gobgp/v3 v3.36.0 h1:6KbNDyvSbN2GAIiVMykAgLUsvcSldNPiCCP5KzV0VP4=
|
||||
github.com/osrg/gobgp/v3 v3.36.0/go.mod h1:ldZ/eydK80FuAmTGYBV23tWhTRJtIk2tw5NSYhPeqVk=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/osrg/gobgp/v3 v3.37.0 h1:+ObuOdvj7G7nxrT0fKFta+EAupdWf/q1WzbXydr8IOY=
|
||||
github.com/osrg/gobgp/v3 v3.37.0/go.mod h1:kVHVFy1/fyZHJ8P32+ctvPeJogn9qKwa1YCeMRXXrP0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
|
||||
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
@@ -205,20 +257,18 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
@@ -232,28 +282,37 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
|
||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -267,45 +326,55 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/etcd/api/v3 v3.6.1 h1:yJ9WlDih9HT457QPuHt/TH/XtsdN2tubyxyQHSHPsEo=
|
||||
go.etcd.io/etcd/api/v3 v3.6.1/go.mod h1:lnfuqoGsXMlZdTJlact3IB56o3bWp1DIlXPIGKRArto=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.1 h1:CxDVv8ggphmamrXM4Of8aCC8QHzDM4tGcVr9p2BSoGk=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.1/go.mod h1:aTkCp+6ixcVTZmrJGa7/Mc5nMNs59PEgBbq+HCmWyMc=
|
||||
go.etcd.io/etcd/client/v3 v3.6.1 h1:KelkcizJGsskUXlsxjVrSmINvMMga0VWwFF0tSPGEP0=
|
||||
go.etcd.io/etcd/client/v3 v3.6.1/go.mod h1:fCbPUdjWNLfx1A6ATo9syUmFVxqHH9bCnPLBZmnLmMY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.etcd.io/etcd/api/v3 v3.6.6 h1:mcaMp3+7JawWv69p6QShYWS8cIWUOl32bFLb6qf8pOQ=
|
||||
go.etcd.io/etcd/api/v3 v3.6.6/go.mod h1:f/om26iXl2wSkcTA1zGQv8reJRSLVdoEBsi4JdfMrx4=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6 h1:uoqgzSOv2H9KlIF5O1Lsd8sW+eMLuV6wzE3q5GJGQNs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.6/go.mod h1:YngfUVmvsvOJ2rRgStIyHsKtOt9SZI2aBJrZiWJhCbI=
|
||||
go.etcd.io/etcd/client/v3 v3.6.6 h1:G5z1wMf5B9SNexoxOHUGBaULurOZPIgGPsW6CN492ec=
|
||||
go.etcd.io/etcd/client/v3 v3.6.6/go.mod h1:36Qv6baQ07znPR3+n7t+Rk5VHEzVYPvFfGmfF4wBHV8=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -326,16 +395,16 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -366,16 +435,16 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -384,8 +453,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -394,14 +463,16 @@ golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uI
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -420,18 +491,18 @@ gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
|
||||
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
|
||||
honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
|
||||
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
|
||||
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
|
||||
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
|
||||
k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
|
||||
k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
|
||||
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
|
||||
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
|
||||
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
|
||||
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
|
||||
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
|
||||
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccorpus2 v1.5.2 h1:Ui+4tc58mf/W+2arcYCJR903y3zl3ecsI7Fpaaqozyw=
|
||||
@@ -450,9 +521,11 @@ pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
|
||||
pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/kind v0.27.0 h1:PQ3f0iAWNIj66LYkZ1ivhEg/+Zb6UPMbO+qVei/INZA=
|
||||
sigs.k8s.io/kind v0.27.0/go.mod h1:RZVFmy6qcwlSWwp6xeIUv7kXCPF3i8MXsEXxW/J+gJY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/kind v0.30.0 h1:2Xi1KFEfSMm0XDcvKnUt15ZfgRPCT0OnCBbpgh8DztY=
|
||||
sigs.k8s.io/kind v0.30.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
112
pkg/arp/arp.go
112
pkg/arp/arp.go
@@ -2,16 +2,18 @@ package arp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
instances map[string]*Instance
|
||||
instances sync.Map
|
||||
config *kubevip.Config
|
||||
}
|
||||
|
||||
@@ -24,8 +26,7 @@ type Instance struct {
|
||||
|
||||
func NewManager(config *kubevip.Config) *Manager {
|
||||
return &Manager{
|
||||
instances: make(map[string]*Instance),
|
||||
config: config,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,10 +43,14 @@ func (i *Instance) Name() string {
|
||||
}
|
||||
|
||||
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
|
||||
i, err := m.get(instance.Name())
|
||||
if err != nil {
|
||||
log.Error("[ARP manager] unable to insert instance", "err", err)
|
||||
return
|
||||
}
|
||||
if i == nil {
|
||||
log.Info("[ARP manager] inserting ARP/NDP instance", "name", instance.Name())
|
||||
m.instances.Store(instance.Name(), instance)
|
||||
} else {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
@@ -54,20 +59,50 @@ func (m *Manager) Insert(instance *Instance) {
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(instance *Instance) {
|
||||
if i, ok := m.instances[instance.Name()]; ok {
|
||||
m.RemoveWithIPDelete(instance, true)
|
||||
}
|
||||
|
||||
// RemoveOnLeadershipLoss removes an ARP instance when leadership is lost
|
||||
func (m *Manager) RemoveOnLeadershipLoss(instance *Instance) {
|
||||
// Use the inverse of PreserveVIPOnLeadershipLoss to decide whether to delete the IP
|
||||
// If preserve is true, don't delete IP (deleteIP = false)
|
||||
// If preserve is false, delete IP (deleteIP = true), This is the legacy behavior
|
||||
deleteIP := !m.config.PreserveVIPOnLeadershipLoss
|
||||
m.RemoveWithIPDelete(instance, deleteIP)
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveWithIPDelete(instance *Instance, deleteIP bool) {
|
||||
i, err := m.get(instance.Name())
|
||||
if err != nil {
|
||||
log.Error("[ARP manager] unable to remove the instance", "err", err)
|
||||
return
|
||||
}
|
||||
if i != nil {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
if i.counter > 1 {
|
||||
i.counter--
|
||||
} else {
|
||||
log.Info("removing ARP/NDP instance", "name", instance.Name())
|
||||
delete(m.instances, instance.Name())
|
||||
log.Info("[ARP manager] removing ARP/NDP instance", "name", instance.Name())
|
||||
if deleteIP {
|
||||
if _, err := instance.network.DeleteIP(); err != nil {
|
||||
log.Error("failed to delete IP", "address", instance.network.IP(), "err", err)
|
||||
}
|
||||
}
|
||||
m.instances.Delete(instance.Name())
|
||||
}
|
||||
} else {
|
||||
log.Warn("[ARP manager] unable to remove the instance - instance not found", "name", instance.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Count(name string) int {
|
||||
if i, ok := m.instances[name]; ok {
|
||||
i, err := m.get(name)
|
||||
if err != nil {
|
||||
log.Error("[ARP manager] unable to count instance", "err", err)
|
||||
return -1
|
||||
}
|
||||
if i != nil {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
return i.counter
|
||||
@@ -76,26 +111,46 @@ func (m *Manager) Count(name string) int {
|
||||
}
|
||||
|
||||
func (m *Manager) StartAdvertisement(ctx context.Context) {
|
||||
log.Info("Starting ARP/NDP advertisement")
|
||||
log.Info("[ARP manager] 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)
|
||||
m.instances.Range(func(_ any, instance any) bool {
|
||||
if i, ok := instance.(*Instance); ok {
|
||||
if i.counter > 0 {
|
||||
ensureIPAndSendGratuitous(i)
|
||||
} else {
|
||||
// this instance should not be advertised - delete the IP just in case...
|
||||
if _, err := i.network.DeleteIP(); err != nil {
|
||||
log.Error("[ARP manager] failed to delete IP", "address", i.network.IP(), "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if m.config.ArpBroadcastRate < 500 {
|
||||
log.Error("arp broadcast rate is too low", "rate (ms)", m.config.ArpBroadcastRate, "setting to (ms)", "3000")
|
||||
log.Warn("[ARP manager] arp broadcast rate is too low", "rate (ms)", m.config.ArpBroadcastRate, "setting to (ms)", "3000")
|
||||
m.config.ArpBroadcastRate = 3000
|
||||
}
|
||||
time.Sleep(time.Duration(m.config.ArpBroadcastRate) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) get(name string) (*Instance, error) {
|
||||
i, exists := m.instances.Load(name)
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
inst, ok := i.(*Instance)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("value for name %q is not of Instance pointer type", name)
|
||||
}
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
// ensureIPAndSendGratuitous - adds IP to the interface if missing, and send
|
||||
// either a gratuitous ARP or gratuitous NDP. Re-adds the interface if it is IPv6
|
||||
// and in a dadfailed state.
|
||||
@@ -111,23 +166,27 @@ func ensureIPAndSendGratuitous(instance *Instance) {
|
||||
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)
|
||||
// }
|
||||
log.Info("deleted and recreating address with NODAD flag to skip DAD", "IP", ipString, "interface", iface)
|
||||
// Re-add immediately without DAD check since we're recovering from DADFAILED
|
||||
// The AddIP function will set IFA_F_NODAD flag for IPv6 addresses when skipDAD=true
|
||||
if _, err := instance.network.AddIP(false, true); err != nil {
|
||||
log.Error("failed to recreate address after DADFAILED", "IP", ipString, "interface", iface, "err", err)
|
||||
} else {
|
||||
log.Info("successfully recreated address after DADFAILED recovery", "IP", ipString, "interface", iface)
|
||||
}
|
||||
}
|
||||
// Return early after DADFAILED recovery to avoid double IP addition
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the address exists on the interface before attempting to ARP
|
||||
// if instance.network.HasEndpoints() {
|
||||
if added, err := instance.network.AddIP(true); err != nil {
|
||||
// Normal case: add IP with precheck and normal DAD process
|
||||
if added, err := instance.network.AddIP(true, false); err != nil {
|
||||
log.Warn(err.Error())
|
||||
} else if added {
|
||||
log.Warn("Re-applied the VIP configuration", "ip", ipString, "interface", iface)
|
||||
}
|
||||
// }
|
||||
|
||||
if vip.IsIPv6(ipString) {
|
||||
if utils.IsIPv6(ipString) {
|
||||
// Gratuitous NDP, will broadcast new MAC <-> IPv6 address
|
||||
if instance.ndp == nil {
|
||||
log.Error("NDP responder was not created")
|
||||
@@ -145,5 +204,4 @@ func ensureIPAndSendGratuitous(instance *Instance) {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -31,7 +30,7 @@ func (e *Entry) Check() bool {
|
||||
// homeConfigPath := filepath.Join(os.Getenv("HOME"), ".kube", "config")
|
||||
|
||||
var k8sAddr string
|
||||
if vip.IsIPv4(e.Addr) {
|
||||
if utils.IsIPv4(e.Addr) {
|
||||
k8sAddr = fmt.Sprintf("%s:%v", e.Addr, e.Port)
|
||||
} else {
|
||||
k8sAddr = fmt.Sprintf("[%s]:%v", e.Addr, e.Port)
|
||||
@@ -40,7 +39,6 @@ func (e *Entry) Check() bool {
|
||||
switch {
|
||||
case utils.FileExists(adminConfigPath):
|
||||
config, err = k8s.NewRestConfig(adminConfigPath, false, k8sAddr)
|
||||
// client, err = k8s.NewClientset(adminConfigPath, false, k8sAddr)
|
||||
if err != nil {
|
||||
log.Error("create k8s REST config", "path", adminConfigPath, "err", err)
|
||||
return false
|
||||
@@ -53,10 +51,6 @@ func (e *Entry) Check() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if using insecure client for this check is OK.
|
||||
// This was changed for more straightforward e2e-test implementation.
|
||||
config.Insecure = true
|
||||
config.CAData = []byte{}
|
||||
client, err = k8s.NewClientset(config)
|
||||
if err != nil {
|
||||
log.Error("create k8s client", "err", err)
|
||||
|
||||
120
pkg/bgp/peers.go
120
pkg/bgp/peers.go
@@ -4,19 +4,19 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
//nolint
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
api "github.com/osrg/gobgp/v3/api"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/osrg/gobgp/v3/pkg/server"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// AddPeer will add peers to the BGP configuration
|
||||
func (b *Server) AddPeer(peer Peer) (err error) {
|
||||
func (b *Server) AddPeer(peer kubevip.BGPPeer) (err error) {
|
||||
p := &api.Peer{
|
||||
Conf: &api.PeerConf{
|
||||
NeighborAddress: peer.Address,
|
||||
@@ -67,9 +67,9 @@ func (b *Server) AddPeer(peer Peer) (err error) {
|
||||
},
|
||||
}
|
||||
|
||||
peer.setMpbgpOptions(b.c)
|
||||
peer.SetMpbgpOptions(b.c)
|
||||
|
||||
ipv4Address, ipv6Address, err := peer.findMpbgpAddresses(p, b.c)
|
||||
ipv4Address, ipv6Address, err := peer.FindMpbgpAddresses(p, b.c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get MP-BGP addresses: %w", err)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (b *Server) AddPeer(peer Peer) (err error) {
|
||||
mask := "128"
|
||||
address := ipv4Address
|
||||
family := api.Family_AFI_IP
|
||||
if vip.IsIPv4(p.Conf.NeighborAddress) {
|
||||
if utils.IsIPv4(p.Conf.NeighborAddress) {
|
||||
mask = "32"
|
||||
address = ipv6Address
|
||||
family = api.Family_AFI_IP6
|
||||
@@ -142,7 +142,7 @@ 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
|
||||
@@ -160,116 +160,18 @@ func (b *Server) getPath(ip net.IP) (path *api.Path) {
|
||||
mpAttr, _ := anypb.New(&api.MpReachNLRIAttribute{
|
||||
Family: v6Family,
|
||||
NextHops: []string{"::"}, // gobgp will fill this
|
||||
Nlris: []*any.Any{nlri},
|
||||
Nlris: []*anypb.Any{nlri},
|
||||
})
|
||||
|
||||
path = &api.Path{
|
||||
Family: v6Family,
|
||||
Nlri: nlri,
|
||||
Pattrs: []*any.Any{originAttr, mpAttr},
|
||||
Pattrs: []*anypb.Any{originAttr, mpAttr},
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseBGPPeerConfig - take a string and parses it into an array of peers
|
||||
func ParseBGPPeerConfig(config string) (bgpPeers []Peer, err error) {
|
||||
peers := strings.Split(config, ",")
|
||||
if len(peers) == 0 {
|
||||
return nil, fmt.Errorf("no BGP Peer configurations found")
|
||||
}
|
||||
|
||||
for x := range peers {
|
||||
peerStr := peers[x]
|
||||
config := strings.Split(peerStr, "/")
|
||||
peerStr = config[0]
|
||||
if peerStr == "" {
|
||||
continue
|
||||
}
|
||||
isV6Peer := peerStr[0] == '['
|
||||
|
||||
address := ""
|
||||
if isV6Peer {
|
||||
addressEndPos := strings.IndexByte(peerStr, ']')
|
||||
if addressEndPos == -1 {
|
||||
return nil, fmt.Errorf("no matching ] found for IPv6 BGP Peer")
|
||||
}
|
||||
address = peerStr[1:addressEndPos]
|
||||
peerStr = peerStr[addressEndPos+1:]
|
||||
}
|
||||
|
||||
peer := strings.Split(peerStr, ":")
|
||||
if len(peer) < 2 {
|
||||
return nil, fmt.Errorf("mandatory peering params <host>:<AS> incomplete")
|
||||
}
|
||||
|
||||
if !isV6Peer {
|
||||
address = peer[0]
|
||||
}
|
||||
|
||||
ASNumber, err := strconv.ParseUint(peer[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP Peer AS format error [%s]", peer[1])
|
||||
}
|
||||
|
||||
password := ""
|
||||
if len(peer) >= 3 {
|
||||
password = peer[2]
|
||||
}
|
||||
|
||||
multiHop := false
|
||||
if len(peer) >= 4 {
|
||||
multiHop, err = strconv.ParseBool(peer[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP MultiHop format error (true/false) [%s]", peer[1])
|
||||
}
|
||||
}
|
||||
|
||||
var port uint64
|
||||
if len(peer) >= 5 {
|
||||
port, err = strconv.ParseUint(peer[4], 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP Peer AS format error [%s]", peer[1])
|
||||
}
|
||||
} else {
|
||||
port = 179
|
||||
}
|
||||
|
||||
var mpbgpNexthop, mpbgpIPv4, mpbgpIPv6 string
|
||||
|
||||
if len(config) > 1 {
|
||||
configData := strings.Split(config[1], ";")
|
||||
for _, cfg := range configData {
|
||||
c := strings.Split(cfg, "=")
|
||||
switch c[0] {
|
||||
case "mpbgp_nexthop":
|
||||
mpbgpNexthop = c[1]
|
||||
case "mpbgp_ipv4":
|
||||
mpbgpIPv4 = c[1]
|
||||
case "mpbgp_ipv6":
|
||||
mpbgpIPv6 = c[1]
|
||||
default:
|
||||
return nil, fmt.Errorf("peer configuration parameter '%s' is not supported", c[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peerConfig := Peer{
|
||||
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 {
|
||||
|
||||
@@ -3,6 +3,8 @@ package bgp
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
)
|
||||
|
||||
func TestParseBGPPeerConfig(t *testing.T) {
|
||||
@@ -12,13 +14,13 @@ func TestParseBGPPeerConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantBgpPeers []Peer
|
||||
wantBgpPeers []kubevip.BGPPeer
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "IPv4, default port",
|
||||
args: args{config: "192.168.0.10:65000::false,192.168.0.11:65000::false"},
|
||||
wantBgpPeers: []Peer{
|
||||
wantBgpPeers: []kubevip.BGPPeer{
|
||||
{Address: "192.168.0.10", Port: 179, AS: 65000, MultiHop: false},
|
||||
{Address: "192.168.0.11", Port: 179, AS: 65000, MultiHop: false},
|
||||
},
|
||||
@@ -26,7 +28,7 @@ func TestParseBGPPeerConfig(t *testing.T) {
|
||||
{
|
||||
name: "IPv4, different port",
|
||||
args: args{config: "192.168.0.10:65000::false:180,192.168.0.11:65000::false:190"},
|
||||
wantBgpPeers: []Peer{
|
||||
wantBgpPeers: []kubevip.BGPPeer{
|
||||
{Address: "192.168.0.10", Port: 180, AS: 65000, MultiHop: false},
|
||||
{Address: "192.168.0.11", Port: 190, AS: 65000, MultiHop: false},
|
||||
},
|
||||
@@ -34,14 +36,14 @@ func TestParseBGPPeerConfig(t *testing.T) {
|
||||
{
|
||||
name: "IPv6, multi-protocol",
|
||||
args: args{config: "[fd00:1111:2222:3333:c7d9:7235:6bf7:5d52]:65501::false/mpbgp_nexthop=auto_sourceif"},
|
||||
wantBgpPeers: []Peer{
|
||||
wantBgpPeers: []kubevip.BGPPeer{
|
||||
{Address: "fd00:1111:2222:3333:c7d9:7235:6bf7:5d52", Port: 179, AS: 65501, MultiHop: false, MpbgpNexthop: "auto_sourceif"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotBgpPeers, err := ParseBGPPeerConfig(tt.args.config)
|
||||
gotBgpPeers, err := kubevip.ParseBGPPeerConfig(tt.args.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseBGPPeerConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
@@ -7,12 +7,24 @@ import (
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
api "github.com/osrg/gobgp/v3/api"
|
||||
gobgp "github.com/osrg/gobgp/v3/pkg/server"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// Server manages a server object
|
||||
type Server struct {
|
||||
s *gobgp.BgpServer
|
||||
c *kubevip.BGPConfig
|
||||
|
||||
// This is a prometheus gauge indicating the state of the sessions.
|
||||
// 1 means "ESTABLISHED", 0 means "NOT ESTABLISHED"
|
||||
BGPSessionInfoGauge *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// NewBGPServer takes a configuration and returns a running BGP server instance
|
||||
func NewBGPServer(c *Config, peerStateChangeCallback func(*api.WatchEventResponse_PeerEvent)) (b *Server, err error) {
|
||||
func NewBGPServer(c kubevip.BGPConfig) (b *Server, err error) {
|
||||
if c.AS == 0 {
|
||||
return nil, fmt.Errorf("you need to provide AS")
|
||||
}
|
||||
@@ -27,14 +39,26 @@ func NewBGPServer(c *Config, peerStateChangeCallback func(*api.WatchEventRespons
|
||||
|
||||
b = &Server{
|
||||
s: gobgp.NewBgpServer(),
|
||||
c: c,
|
||||
c: &c,
|
||||
|
||||
BGPSessionInfoGauge: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: "kube_vip",
|
||||
Subsystem: "manager",
|
||||
Name: "bgp_session_info",
|
||||
Help: "Display state of session by setting metric for label value with current state to 1",
|
||||
}, []string{"state", "peer"}),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Start starts the BGP server
|
||||
func (b *Server) Start(peerStateChangeCallback func(*api.WatchEventResponse_PeerEvent)) (err error) {
|
||||
go b.s.Serve()
|
||||
|
||||
if err = b.s.StartBgp(context.Background(), &api.StartBgpRequest{
|
||||
Global: &api.Global{
|
||||
Asn: c.AS,
|
||||
RouterId: c.RouterID,
|
||||
Asn: b.c.AS,
|
||||
RouterId: b.c.RouterID,
|
||||
ListenPort: -1,
|
||||
},
|
||||
}); err != nil {
|
||||
@@ -52,12 +76,23 @@ func NewBGPServer(c *Config, peerStateChangeCallback func(*api.WatchEventRespons
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range c.Peers {
|
||||
for _, p := range b.c.Peers {
|
||||
if err = b.AddPeer(p); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if b.c.Zebra.Enabled {
|
||||
if err = b.s.EnableZebra(context.Background(), &api.EnableZebraRequest{
|
||||
Url: b.c.Zebra.URL,
|
||||
Version: b.c.Zebra.Version,
|
||||
SoftwareName: b.c.Zebra.SoftwareName,
|
||||
}); err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
130
pkg/bgp/types.go
130
pkg/bgp/types.go
@@ -1,130 +0,0 @@
|
||||
package bgp
|
||||
|
||||
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/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// Peer defines a BGP Peer
|
||||
type Peer struct {
|
||||
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
|
||||
MpbgpNexthop string
|
||||
MpbgpIPv4 string
|
||||
MpbgpIPv6 string
|
||||
|
||||
HoldTime uint64
|
||||
KeepaliveInterval uint64
|
||||
|
||||
Peers []Peer
|
||||
}
|
||||
|
||||
// Server manages a server object
|
||||
type Server struct {
|
||||
s *gobgp.BgpServer
|
||||
c *Config
|
||||
}
|
||||
@@ -55,7 +55,8 @@ func startNetworking(c *kubevip.Config, intfMgr *networkinterface.Manager) ([]vi
|
||||
networks := []vip.Network{}
|
||||
for _, addr := range addresses {
|
||||
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, intfMgr)
|
||||
c.RoutingTableType, c.RoutingProtocol, c.DNSMode, c.LoadBalancerForwardingMethod, c.IptablesBackend,
|
||||
c.EnableLoadBalancer, c.EnableServiceSecurity, intfMgr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -12,16 +12,14 @@ import (
|
||||
// during runtime if IP changes, startDDNS don't have to do reconfigure because
|
||||
// dnsUpdater already have the functionality to keep trying resolve the IP
|
||||
// and update the VIP configuration if it changes
|
||||
func (cluster *Cluster) StartDDNS(ctx context.Context) error {
|
||||
for i := range cluster.Network {
|
||||
ddnsMgr := vip.NewDDNSManager(ctx, cluster.Network[i])
|
||||
ip, err := ddnsMgr.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cluster.Network[i].SetIP(ip); err != nil {
|
||||
return err
|
||||
}
|
||||
func (cluster *Cluster) StartDDNS(ctx context.Context, network vip.Network) error {
|
||||
ddnsMgr := vip.NewDDNSManager(ctx, network)
|
||||
ip, err := ddnsMgr.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = network.SetIP(ip); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/kube-vip/kube-vip/pkg/k8s"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/loadbalancer"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
@@ -151,10 +152,13 @@ func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *
|
||||
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("new BGP server", "err", err)
|
||||
}
|
||||
if err := bgpServer.Start(nil); err != nil {
|
||||
log.Error("starting BGP server", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
run := &runConfig{
|
||||
@@ -162,6 +166,26 @@ func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *
|
||||
leaseID: c.NodeName,
|
||||
sm: sm,
|
||||
onStartedLeading: func(ctx context.Context) { //nolint TODO: potential clean code
|
||||
// When we become leader, ensure we can take over VIPs even if they're preserved on other nodes
|
||||
if c.PreserveVIPOnLeadershipLoss {
|
||||
log.Info("Becoming leader with VIP preservation enabled - ensuring VIP takeover")
|
||||
// Force add the VIPs (this will work even if they exist due to the precheck logic)
|
||||
for i := range cluster.Network {
|
||||
added, err := cluster.Network[i].AddIP(true, false)
|
||||
if err != nil {
|
||||
log.Error("failed to ensure VIP on leader takeover", "vip", cluster.Network[i].IP(), "err", err)
|
||||
} else if added {
|
||||
log.Info("took over VIP as new leader", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
} else {
|
||||
log.Info("VIP already configured on interface", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start ARP advertisements now that we have leadership
|
||||
log.Info("Start ARP/NDP advertisement")
|
||||
go cluster.arpMgr.StartAdvertisement(ctxArp)
|
||||
|
||||
// As we're leading lets start the vip service
|
||||
err := cluster.vipService(ctxArp, ctxDNS, c, sm, bgpServer, cancel)
|
||||
if err != nil {
|
||||
@@ -185,13 +209,36 @@ func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *
|
||||
}
|
||||
}
|
||||
|
||||
for i := range cluster.Network {
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("delete VIP", "err", err)
|
||||
// Handle VIP cleanup based on configuration
|
||||
if c.PreserveVIPOnLeadershipLoss {
|
||||
// For IPv6, we must remove VIPs immediately to avoid DAD failures on the new leader
|
||||
// IPv6 Duplicate Address Detection will fail if the new leader tries to add an IP that is still present on this node's interface
|
||||
// We need to check each VIP individually and only remove IPv6 VIPs
|
||||
for i := range cluster.Network {
|
||||
if utils.IsIPv6(cluster.Network[i].IP()) {
|
||||
log.Info("Removing IPv6 VIP immediately (required to prevent DAD failures on new leader)", "ip", cluster.Network[i].IP())
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("delete VIP", "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
} else {
|
||||
log.Info("Preserving IPv4 VIP address on interface, only stopped ARP broadcasting", "ip", cluster.Network[i].IP())
|
||||
}
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
} else {
|
||||
// Legacy behavior: delete VIP addresses on leadership loss
|
||||
log.Info("Deleting VIP addresses on leadership loss (legacy behavior)")
|
||||
for i := range cluster.Network {
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("delete VIP", "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +248,23 @@ func (cluster *Cluster) StartCluster(c *kubevip.Config, sm *Manager, bgpServer *
|
||||
onNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
log.Info("New leader", "leader", identity)
|
||||
|
||||
// If we're not the new leader and we have VIPs preserved from previous leadership,
|
||||
// we need to clean them up to avoid conflicts.
|
||||
if identity != c.NodeName && c.PreserveVIPOnLeadershipLoss {
|
||||
log.Info("Cleaning up preserved VIPs as another node became leader", "new_leader", identity)
|
||||
for i := range cluster.Network {
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn("failed to cleanup preserved VIP", "vip", cluster.Network[i].IP(), "err", err)
|
||||
}
|
||||
if deleted {
|
||||
log.Info("cleaned up preserved VIP to avoid conflict", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface(), "new_leader", identity)
|
||||
} else {
|
||||
log.Debug("VIP was not present on this node", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -281,7 +345,7 @@ func (cluster *Cluster) runEtcdLeaderElectionOrDie(ctx context.Context, run *run
|
||||
})
|
||||
}
|
||||
|
||||
func (sm *Manager) NodeWatcher(lb *loadbalancer.IPVSLoadBalancer, port uint16) 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.Info("Kube-Vip is watching nodes for control-plane labels")
|
||||
|
||||
@@ -289,7 +353,7 @@ func (sm *Manager) NodeWatcher(lb *loadbalancer.IPVSLoadBalancer, port uint16) e
|
||||
LabelSelector: "node-role.kubernetes.io/control-plane",
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcher("1", &cache.ListWatch{
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctxArp, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.RetryWatcherClient.CoreV1().Nodes().Watch(context.Background(), listOptions)
|
||||
},
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/loadbalancer"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
"github.com/vishvananda/netlink"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -45,15 +46,17 @@ func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Co
|
||||
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
|
||||
if network.IsDDNS() {
|
||||
if err := cluster.StartDDNS(ctxDNS, cluster.Network[i]); err != nil {
|
||||
log.Error("failed to start DDNS", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := network.SetMask(c.VIPSubnet); err != nil {
|
||||
log.Error("failed to set mask", "subnet", c.VIPSubnet, "err", err)
|
||||
panic("")
|
||||
}
|
||||
if network.IsDDNS() {
|
||||
if err := cluster.StartDDNS(ctxDNS); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// start the dns updater if address is dns
|
||||
if network.IsDNS() {
|
||||
@@ -63,7 +66,8 @@ func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Co
|
||||
}
|
||||
|
||||
if !c.EnableRoutingTable {
|
||||
if _, err = network.AddIP(false); err != nil {
|
||||
// Normal VIP addition, use skipDAD=false for normal DAD process
|
||||
if _, err = network.AddIP(false, false); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -84,7 +88,7 @@ func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Co
|
||||
}
|
||||
|
||||
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.Error("Error watching node labels", "err", err)
|
||||
}
|
||||
@@ -173,6 +177,7 @@ func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Co
|
||||
log.Error("failed to check IP type", "IP", networkIP, "error", err)
|
||||
continue
|
||||
}
|
||||
log.Debug("current ip to process", "ip", networkIP)
|
||||
|
||||
backendMap := &backendMapV4
|
||||
if isNetworkV6 {
|
||||
@@ -180,8 +185,11 @@ func (cluster *Cluster) vipService(ctxArp, ctxDNS context.Context, c *kubevip.Co
|
||||
}
|
||||
|
||||
for entry := range *backendMap {
|
||||
log.Debug("entry.Check() for entry", "entry", entry)
|
||||
if entry.Check() {
|
||||
_, err = network.AddIP(true)
|
||||
log.Debug("entry.Check() true")
|
||||
// Normal VIP addition with precheck, use skipDAD=false for normal DAD process
|
||||
_, err = network.AddIP(true, false)
|
||||
if err != nil {
|
||||
log.Error("error adding address", "err", err)
|
||||
}
|
||||
@@ -257,19 +265,21 @@ func getNodeIPs(ctx context.Context, nodename string, client *kubernetes.Clients
|
||||
}
|
||||
|
||||
// StartLoadBalancerService will start a VIP instance and leave it for kube-proxy to handle
|
||||
func (cluster *Cluster) StartLoadBalancerService(c *kubevip.Config, bgp *bgp.Server, name string, serviceInstances *[]*Instance) {
|
||||
func (cluster *Cluster) StartLoadBalancerService(ctx context.Context, c *kubevip.Config, bgp *bgp.Server, name string, CountRouteReferences func(*netlink.Route) int) {
|
||||
// use a Go context so we can tell the arp loop code when we
|
||||
// want to step down
|
||||
//nolint
|
||||
ctxArp, cancelArp := context.WithCancel(context.Background())
|
||||
ctxArp, cancelArp := context.WithCancel(ctx)
|
||||
|
||||
cluster.stop = make(chan bool, 1)
|
||||
cluster.completed = make(chan bool, 1)
|
||||
|
||||
var arpWG sync.WaitGroup
|
||||
|
||||
log.Debug("StartLoadBalancerService")
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
log.Debug("current ip to process", "ip", network.IP(), "mask", c.VIPSubnet)
|
||||
if err := network.SetMask(c.VIPSubnet); err != nil {
|
||||
log.Error("failed to set mask", "subnet", c.VIPSubnet, "err", err)
|
||||
panic("")
|
||||
@@ -278,16 +288,22 @@ func (cluster *Cluster) StartLoadBalancerService(c *kubevip.Config, bgp *bgp.Ser
|
||||
if err != nil {
|
||||
log.Warn("attempted to clean existing VIP", "err", err)
|
||||
}
|
||||
log.Debug("config flags", "enable_routing_table", c.EnableRoutingTable, "enable_leader_election", c.EnableLeaderElection, "enable_services_election", c.EnableServicesElection)
|
||||
|
||||
if c.EnableRoutingTable && (c.EnableLeaderElection || c.EnableServicesElection) {
|
||||
err = network.AddRoute(false)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
} else {
|
||||
log.Info("successful add Route")
|
||||
}
|
||||
} else if !c.EnableRoutingTable {
|
||||
if _, err = network.AddIP(false); err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Normal VIP addition, use skipDAD=false for normal DAD process
|
||||
if _, err = network.AddIP(false, false); err != nil {
|
||||
log.Warn(err.Error())
|
||||
} else {
|
||||
log.Info("successful add IP")
|
||||
}
|
||||
|
||||
if c.EnableARP {
|
||||
@@ -318,7 +334,7 @@ func (cluster *Cluster) StartLoadBalancerService(c *kubevip.Config, bgp *bgp.Ser
|
||||
for i := range cluster.Network {
|
||||
// chek if route is not referenced by another service
|
||||
r := cluster.Network[i].PrepareRoute()
|
||||
if CountRouteReferences(serviceInstances, r) < 1 {
|
||||
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())
|
||||
@@ -330,16 +346,37 @@ func (cluster *Cluster) StartLoadBalancerService(c *kubevip.Config, bgp *bgp.Ser
|
||||
return
|
||||
}
|
||||
for i := range cluster.Network {
|
||||
if c.EnableARP && cluster.arpMgr.Count(cluster.Network[i].ARPName()) > 0 {
|
||||
if c.EnableARP && cluster.arpMgr.Count(cluster.Network[i].ARPName()) > 1 {
|
||||
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())
|
||||
|
||||
// Handle VIP cleanup based on configuration
|
||||
if c.PreserveVIPOnLeadershipLoss {
|
||||
// For IPv6, we must remove VIPs immediately to avoid DAD failures on the new leader
|
||||
// IPv6 Duplicate Address Detection will fail if the new leader tries to add an IP
|
||||
// that is still present on this node's interface
|
||||
if utils.IsIPv6(cluster.Network[i].IP()) {
|
||||
log.Info("[VIP] Removing IPv6 VIP immediately (required to prevent DAD failures on new leader)", "ip", cluster.Network[i].IP())
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
} else {
|
||||
log.Info("[VIP] Preserving IPv4 VIP address on interface, only stopped ARP broadcasting", "ip", cluster.Network[i].IP())
|
||||
}
|
||||
} else {
|
||||
// Legacy behavior: delete VIP addresses on leadership loss
|
||||
log.Info("[VIP] Deleting VIP", "ip", cluster.Network[i].IP())
|
||||
deleted, err := cluster.Network[i].DeleteIP()
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
if deleted {
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,7 +391,7 @@ func (cluster *Cluster) layer2Update(ctx context.Context, network vip.Network, c
|
||||
var ndp *vip.NdpResponder
|
||||
var err error
|
||||
ipString := network.IP()
|
||||
if vip.IsIPv6(ipString) {
|
||||
if utils.IsIPv6(ipString) {
|
||||
if network.IPisLinkLocal() {
|
||||
log.Error("layer2 is link-local can't use NDP", "address", ipString)
|
||||
|
||||
@@ -377,24 +414,5 @@ func (cluster *Cluster) layer2Update(ctx context.Context, network vip.Network, c
|
||||
|
||||
<-ctx.Done() // if cancel() execute
|
||||
log.Debug("ending layer 2 update", "ip", ipString, "interface", network.Interface(), "ms", c.ArpBroadcastRate)
|
||||
cluster.arpMgr.Remove(arpInstance)
|
||||
}
|
||||
|
||||
func CountRouteReferences(serviceInstances *[]*Instance, route *netlink.Route) int {
|
||||
// Count how many service instances have the same route
|
||||
// This function is not thread-safe, it should be called only from the main thread with a lock held
|
||||
cnt := 0
|
||||
for _, instance := range *serviceInstances {
|
||||
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
|
||||
cluster.arpMgr.RemoveOnLeadershipLoss(arpInstance)
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ func (cluster *Cluster) StartSingleNode(c *kubevip.Config, disableVIP bool) erro
|
||||
log.Info("deleted address", "IP", cluster.Network[i].IP(), "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
|
||||
_, err = cluster.Network[i].AddIP(false)
|
||||
// Normal VIP addition for single node, use skipDAD=false for normal DAD process
|
||||
_, err = cluster.Network[i].AddIP(false, false)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
|
||||
100
pkg/egress/egress.go
Normal file
100
pkg/egress/egress.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package egress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/iptables"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/nftables"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
func Teardown(podIP, vipIP, namespace, serviceUUID string, annotations map[string]string, useNftables bool) error {
|
||||
// Look up the destination ports from the annotations on the service
|
||||
destinationPorts := annotations[kubevip.EgressDestinationPorts]
|
||||
deniedNetworks := annotations[kubevip.EgressDeniedNetworks]
|
||||
allowedNetworks := annotations[kubevip.EgressAllowedNetworks]
|
||||
internalEgress := annotations[kubevip.EgressInternal]
|
||||
|
||||
protocol := iptables.ProtocolIPv4
|
||||
IPv6 := false
|
||||
if utils.IsIPv6(podIP) {
|
||||
protocol = iptables.ProtocolIPv6
|
||||
IPv6 = true
|
||||
}
|
||||
|
||||
// Use the internal egress implementation
|
||||
if internalEgress != "" {
|
||||
return nftables.DeleteSNAT(IPv6, serviceUUID)
|
||||
}
|
||||
|
||||
i, err := vip.CreateIptablesClient(useNftables, namespace, protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error Creating iptables client [%s]", err)
|
||||
}
|
||||
|
||||
if deniedNetworks != "" {
|
||||
networks := strings.Split(deniedNetworks, ",")
|
||||
for x := range networks {
|
||||
err = i.DeleteMangleReturnForNetwork(vip.MangleChainName, networks[x])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting rules in mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowedNetworks != "" {
|
||||
networks := strings.Split(allowedNetworks, ",")
|
||||
for x := range networks {
|
||||
err = i.DeleteMangleMarkingForNetwork(podIP, vip.MangleChainName, networks[x])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting rules in mangle chain [%s], error [%s]", vip.MangleChainName, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove the marking of egress packets
|
||||
err = i.DeleteMangleMarking(podIP, vip.MangleChainName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear up SNAT rules
|
||||
if destinationPorts != "" {
|
||||
fixedPorts := strings.Split(destinationPorts, ",")
|
||||
|
||||
for _, fixedPort := range fixedPorts {
|
||||
var proto, port string
|
||||
|
||||
data := strings.Split(fixedPort, ":")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
} else if len(data) == 1 {
|
||||
proto = "tcp"
|
||||
port = data[0]
|
||||
} else {
|
||||
proto = data[0]
|
||||
port = data[1]
|
||||
}
|
||||
|
||||
err = i.DeleteSourceNatForDestinationPort(podIP, vipIP, port, proto)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
err = i.DeleteSourceNat(podIP, vipIP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = vip.DeleteExistingSessions(podIP, false, destinationPorts, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error changing iptables rules for egress [%s]", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
165
pkg/endpoints/endpoints.go
Normal file
165
pkg/endpoints/endpoints.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
config *kubevip.Config
|
||||
provider providers.Provider
|
||||
bgpServer *bgp.Server
|
||||
worker endpointWorker
|
||||
instances *[]*instance.Instance
|
||||
}
|
||||
|
||||
func NewEndpointProcessor(config *kubevip.Config, provider providers.Provider, bgpServer *bgp.Server,
|
||||
instances *[]*instance.Instance) *Processor {
|
||||
return &Processor{
|
||||
config: config,
|
||||
provider: provider,
|
||||
bgpServer: bgpServer,
|
||||
instances: instances,
|
||||
worker: newEndpointWorker(config, provider, bgpServer, instances),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) AddOrModify(ctx *servicecontext.Context, event watch.Event,
|
||||
lastKnownGoodEndpoint *string, service *v1.Service, id string, leaderElectionActive *bool,
|
||||
serviceFunc func(context.Context, *v1.Service) error,
|
||||
leaderCtx *context.Context, cancel *context.CancelFunc) (bool, error) {
|
||||
|
||||
var err error
|
||||
if err = p.provider.LoadObject(event.Object, *cancel); err != nil {
|
||||
return false, fmt.Errorf("[%s] error loading k8s object: %w", p.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
endpoints, err := p.worker.getEndpoints(service, id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := p.worker.setInstanceEndpointsStatus(service, endpoints); err != nil {
|
||||
log.Error("updating instance", "err", err)
|
||||
}
|
||||
|
||||
// Find out if we have any local endpoints
|
||||
// if out endpoint is empty then populate it
|
||||
// if not, go through the endpoints and see if ours still exists
|
||||
// If we have a local endpoint then begin the leader Election, unless it's already running
|
||||
//
|
||||
|
||||
// Check that we have local endpoints
|
||||
if len(endpoints) != 0 {
|
||||
// Ignore IPv4
|
||||
if service.Annotations[kubevip.EgressIPv6] == "true" && net.ParseIP(endpoints[0]).To4() != nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
p.updateLastKnownGoodEndpoint(lastKnownGoodEndpoint, endpoints, service, leaderElectionActive, *cancel)
|
||||
// start leader election if it's enabled and not already started
|
||||
if !*leaderElectionActive && p.config.EnableServicesElection {
|
||||
go func() {
|
||||
*leaderCtx, *cancel = context.WithCancel(ctx.Ctx)
|
||||
startLeaderElection(*leaderCtx, leaderElectionActive, service, serviceFunc)
|
||||
}()
|
||||
}
|
||||
|
||||
// There are local endpoints available on the node
|
||||
if !p.config.EnableServicesElection && !p.config.EnableLeaderElection {
|
||||
if err := p.worker.processInstance(ctx, service, leaderElectionActive); err != nil {
|
||||
return false, fmt.Errorf("failed to process non-empty instance: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There are no local endpoints
|
||||
p.worker.clear(ctx, lastKnownGoodEndpoint, service, *cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
// Set the service accordingly
|
||||
p.updateAnnotations(service, lastKnownGoodEndpoint)
|
||||
|
||||
log.Debug("watcher", "provider",
|
||||
p.provider.GetLabel(), "service name", service.Name, "namespace", service.Namespace, "endpoints", len(endpoints), "last endpoint", *lastKnownGoodEndpoint, "active leader election", *leaderElectionActive)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *Processor) Delete(service *v1.Service, id string) error {
|
||||
if err := p.worker.delete(service, id); err != nil {
|
||||
return fmt.Errorf("[%s] error deleting service: %w", p.provider.GetLabel(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) updateLastKnownGoodEndpoint(lastKnownGoodEndpoint *string, endpoints []string, service *v1.Service, leaderElectionActive *bool, cancel context.CancelFunc) {
|
||||
// if we haven't populated one, then do so
|
||||
if *lastKnownGoodEndpoint == "" {
|
||||
*lastKnownGoodEndpoint = endpoints[0]
|
||||
return
|
||||
}
|
||||
|
||||
// check out previous endpoint exists
|
||||
stillExists := false
|
||||
|
||||
for x := range endpoints {
|
||||
if endpoints[x] == *lastKnownGoodEndpoint {
|
||||
stillExists = true
|
||||
}
|
||||
}
|
||||
// If the last endpoint no longer exists, we cancel our leader Election, and set another endpoint as last known good
|
||||
if !stillExists {
|
||||
p.worker.removeEgress(service, lastKnownGoodEndpoint)
|
||||
if *leaderElectionActive && (p.config.EnableServicesElection || p.config.EnableLeaderElection) {
|
||||
log.Warn("existing endpoint has been removed, restarting leaderElection", "provider", p.provider.GetLabel(), "endpoint", *lastKnownGoodEndpoint)
|
||||
// Stop the existing leaderElection
|
||||
cancel()
|
||||
// disable last leaderElection flag
|
||||
*leaderElectionActive = false
|
||||
}
|
||||
// Set our active endpoint to an existing one
|
||||
*lastKnownGoodEndpoint = endpoints[0]
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) updateAnnotations(service *v1.Service, lastKnownGoodEndpoint *string) {
|
||||
// Set the service accordingly
|
||||
if service.Annotations[kubevip.Egress] == "true" {
|
||||
activeEndpointAnnotation := kubevip.ActiveEndpoint
|
||||
|
||||
if !p.config.EnableEndpoints && p.provider.GetProtocol() == string(discoveryv1.AddressTypeIPv6) {
|
||||
activeEndpointAnnotation = kubevip.ActiveEndpointIPv6
|
||||
}
|
||||
service.Annotations[activeEndpointAnnotation] = *lastKnownGoodEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
func startLeaderElection(ctx context.Context, leaderElectionActive *bool, service *v1.Service, serviceFunc func(context.Context, *v1.Service) error) {
|
||||
// This is a blocking function, that will restart (in the event of failure)
|
||||
for {
|
||||
// if the context isn't cancelled restart
|
||||
if ctx.Err() != context.Canceled {
|
||||
*leaderElectionActive = true
|
||||
err := serviceFunc(ctx, service)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
*leaderElectionActive = false
|
||||
} else {
|
||||
*leaderElectionActive = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
129
pkg/endpoints/endpoints_bgp.go
Normal file
129
pkg/endpoints/endpoints_bgp.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type BGP struct {
|
||||
generic
|
||||
bgpServer *bgp.Server
|
||||
}
|
||||
|
||||
func newBGP(generic generic, bgpServer *bgp.Server) endpointWorker {
|
||||
return &BGP{
|
||||
generic: generic,
|
||||
bgpServer: bgpServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BGP) processInstance(ctx *servicecontext.Context, service *v1.Service, leaderElectionActive *bool) error {
|
||||
if instance := instance.FindServiceInstance(service, *b.instances); instance != nil {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
if !ctx.IsNetworkConfigured(cluster.Network[i].IP()) {
|
||||
log.Debug("attempting to advertise BGP service", "provider", b.provider.GetLabel(), "ip", cluster.Network[i].IP())
|
||||
err := b.bgpServer.AddHost(cluster.Network[i].CIDR())
|
||||
if err != nil {
|
||||
log.Error("error adding BGP host", "provider", b.provider.GetLabel(), "err", err)
|
||||
} else {
|
||||
log.Info("added BGP host", "provider",
|
||||
b.provider.GetLabel(), "ip", cluster.Network[i].CIDR(), "service name", service.Name, "namespace", service.Namespace)
|
||||
ctx.ConfiguredNetworks.Store(cluster.Network[i].IP(), true)
|
||||
*leaderElectionActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BGP) clear(ctx *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
if !b.config.EnableServicesElection && !b.config.EnableLeaderElection {
|
||||
// If BGP mode is enabled - routes should be deleted
|
||||
if instance := instance.FindServiceInstance(service, *b.instances); instance != nil {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
err := b.bgpServer.DelHost(cluster.Network[i].CIDR())
|
||||
if err != nil {
|
||||
log.Error("deleting BGP host", "provider", b.provider.GetLabel(), "ip", cluster.Network[i].IP(), "err", err)
|
||||
} else {
|
||||
log.Info("deleted BGP host", "provider",
|
||||
b.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace", service.Namespace)
|
||||
ctx.ConfiguredNetworks.Delete(cluster.Network[i].IP())
|
||||
*leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
b.clearEgress(lastKnownGoodEndpoint, service, cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
func (b *BGP) getEndpoints(service *v1.Service, id string) ([]string, error) {
|
||||
return b.getAllEndpoints(service, id)
|
||||
}
|
||||
|
||||
func (b *BGP) delete(service *v1.Service, id string) error {
|
||||
// When no-leader-elecition mode
|
||||
if !b.config.EnableServicesElection && !b.config.EnableLeaderElection {
|
||||
// find all existing local endpoints
|
||||
endpoints, err := b.getEndpoints(service, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error getting endpoints: %w", b.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
// If there were local endpoints deleted
|
||||
if len(endpoints) > 0 {
|
||||
b.deleteAction(service)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BGP) deleteAction(service *v1.Service) {
|
||||
b.clearBGPHosts(service)
|
||||
}
|
||||
|
||||
func (b *BGP) clearBGPHosts(service *v1.Service) {
|
||||
ClearBGPHosts(service, b.instances, b.bgpServer)
|
||||
}
|
||||
|
||||
func (b *BGP) setInstanceEndpointsStatus(_ *v1.Service, _ []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearBGPHosts(service *v1.Service, instances *[]*instance.Instance, bgpServer *bgp.Server) {
|
||||
if instance := instance.FindServiceInstance(service, *instances); instance != nil {
|
||||
ClearBGPHostsByInstance(instance, bgpServer)
|
||||
}
|
||||
}
|
||||
|
||||
func ClearBGPHostsByInstance(instance *instance.Instance, bgpServer *bgp.Server) {
|
||||
if instance == nil {
|
||||
log.Error("failed to clear BGP host for nil instance")
|
||||
return
|
||||
}
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
err := bgpServer.DelHost(network.CIDR())
|
||||
if err != nil {
|
||||
log.Error("[endpoint] error deleting BGP host", "err", err)
|
||||
} else {
|
||||
log.Debug("[endpoint] deleted BGP host", "ip",
|
||||
network.CIDR(), "service name", instance.ServiceSnapshot.Name, "namespace", instance.ServiceSnapshot.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
pkg/endpoints/endpoints_generic.go
Normal file
118
pkg/endpoints/endpoints_generic.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/egress"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type endpointWorker interface {
|
||||
processInstance(svcCtx *servicecontext.Context, service *v1.Service, leaderElectionActive *bool) error
|
||||
clear(svcCtx *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool)
|
||||
getEndpoints(service *v1.Service, id string) ([]string, error)
|
||||
removeEgress(service *v1.Service, lastKnownGoodEndpoint *string)
|
||||
delete(service *v1.Service, id string) error
|
||||
setInstanceEndpointsStatus(service *v1.Service, endpoints []string) error
|
||||
}
|
||||
|
||||
func newEndpointWorker(config *kubevip.Config, provider providers.Provider, bgpServer *bgp.Server, instances *[]*instance.Instance) endpointWorker {
|
||||
generic := newGeneric(config, provider, instances)
|
||||
|
||||
if config.EnableRoutingTable {
|
||||
return newRoutingTable(generic)
|
||||
}
|
||||
if config.EnableBGP {
|
||||
return newBGP(generic, bgpServer)
|
||||
}
|
||||
|
||||
return &generic
|
||||
}
|
||||
|
||||
type generic struct {
|
||||
config *kubevip.Config
|
||||
provider providers.Provider
|
||||
instances *[]*instance.Instance
|
||||
}
|
||||
|
||||
func newGeneric(config *kubevip.Config, provider providers.Provider, instances *[]*instance.Instance) generic {
|
||||
return generic{
|
||||
config: config,
|
||||
provider: provider,
|
||||
instances: instances,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generic) processInstance(_ *servicecontext.Context, _ *v1.Service, _ *bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *generic) clear(_ *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
g.clearEgress(lastKnownGoodEndpoint, service, cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
func (g *generic) clearEgress(lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
if *lastKnownGoodEndpoint != "" {
|
||||
log.Warn("existing endpoint has been removed, no remaining endpoints for leaderElection", "provider", g.provider.GetLabel(), "endpoint", lastKnownGoodEndpoint)
|
||||
if err := egress.Teardown(*lastKnownGoodEndpoint, service.Spec.LoadBalancerIP, service.Namespace, string(service.UID), service.Annotations, g.config.EgressWithNftables); err != nil {
|
||||
log.Error("error removing redundant egress rules", "err", err)
|
||||
}
|
||||
|
||||
*lastKnownGoodEndpoint = "" // reset endpoint
|
||||
if g.config.EnableServicesElection || g.config.EnableLeaderElection {
|
||||
cancel() // stop services watcher
|
||||
}
|
||||
*leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
|
||||
func (g *generic) getEndpoints(_ *v1.Service, id string) ([]string, error) {
|
||||
return g.getLocalEndpoints(id)
|
||||
}
|
||||
|
||||
func (g *generic) getLocalEndpoints(id string) ([]string, error) {
|
||||
// Build endpoints
|
||||
var endpoints []string
|
||||
var err error
|
||||
if endpoints, err = g.provider.GetLocalEndpoints(id, g.config); err != nil {
|
||||
return nil, fmt.Errorf("[%s] error getting local endpoints: %w", g.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (g *generic) getAllEndpoints(service *v1.Service, id string) ([]string, error) {
|
||||
// Build endpoints
|
||||
var err error
|
||||
var endpoints []string
|
||||
if !g.config.EnableLeaderElection && !g.config.EnableServicesElection &&
|
||||
service.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
if endpoints, err = g.provider.GetAllEndpoints(); err != nil {
|
||||
return nil, fmt.Errorf("[%s] error getting all endpoints: %w", g.provider.GetLabel(), err)
|
||||
}
|
||||
} else {
|
||||
if endpoints, err = g.provider.GetLocalEndpoints(id, g.config); err != nil {
|
||||
return nil, fmt.Errorf("[%s] error getting local endpoints: %w", g.provider.GetLabel(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (g *generic) removeEgress(_ *v1.Service, _ *string) {
|
||||
}
|
||||
|
||||
func (g *generic) delete(_ *v1.Service, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *generic) setInstanceEndpointsStatus(_ *v1.Service, _ []string) error {
|
||||
return nil
|
||||
}
|
||||
189
pkg/endpoints/endpoints_routing_table.go
Normal file
189
pkg/endpoints/endpoints_routing_table.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/egress"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type RoutingTable struct {
|
||||
generic
|
||||
}
|
||||
|
||||
func newRoutingTable(generic generic) endpointWorker {
|
||||
return &RoutingTable{
|
||||
generic: generic,
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) processInstance(ctx *servicecontext.Context, service *v1.Service, leaderElectionActive *bool) error {
|
||||
instance := instance.FindServiceInstance(service, *rt.instances)
|
||||
if instance != nil {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
if !ctx.IsNetworkConfigured(cluster.Network[i].IP()) && cluster.Network[i].HasEndpoints() {
|
||||
err := cluster.Network[i].AddRoute(false)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EEXIST) {
|
||||
// If route exists, but protocol is not set (e.g. the route was created by the older version
|
||||
// of kube-vip) try to update it if necessary
|
||||
isUpdated, err := cluster.Network[i].UpdateRoutes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error updating existing routes: %w", rt.provider.GetLabel(), err)
|
||||
}
|
||||
if isUpdated {
|
||||
log.Info("updated route", "provider",
|
||||
rt.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace",
|
||||
service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", rt.config.RoutingTableID)
|
||||
} else {
|
||||
log.Info("route already present", "provider",
|
||||
rt.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace",
|
||||
service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", rt.config.RoutingTableID)
|
||||
}
|
||||
} else {
|
||||
// If other error occurs, return error
|
||||
return fmt.Errorf("[%s] error adding route: %s", rt.provider.GetLabel(), err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Info("added route", "provider",
|
||||
rt.provider.GetLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace",
|
||||
service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", rt.config.RoutingTableID)
|
||||
ctx.ConfiguredNetworks.Store(cluster.Network[i].IP(), true)
|
||||
*leaderElectionActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) clear(svcCtx *servicecontext.Context, lastKnownGoodEndpoint *string, service *v1.Service, cancel context.CancelFunc, leaderElectionActive *bool) {
|
||||
if !rt.config.EnableServicesElection && !rt.config.EnableLeaderElection {
|
||||
if errs := ClearRoutes(service, rt.instances); len(errs) == 0 {
|
||||
svcCtx.ConfiguredNetworks.Clear()
|
||||
} else {
|
||||
for _, err := range errs {
|
||||
log.Error("error while clearing routes", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rt.clearEgress(lastKnownGoodEndpoint, service, cancel, leaderElectionActive)
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) getEndpoints(service *v1.Service, id string) ([]string, error) {
|
||||
return rt.getAllEndpoints(service, id)
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) removeEgress(service *v1.Service, lastKnownGoodEndpoint *string) {
|
||||
if err := egress.Teardown(*lastKnownGoodEndpoint, service.Spec.LoadBalancerIP,
|
||||
service.Namespace, string(service.UID), service.Annotations, rt.config.EgressWithNftables); err != nil {
|
||||
log.Warn("removing redundant egress rules", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) delete(service *v1.Service, id string) error {
|
||||
// When no-leader-elecition mode
|
||||
if !rt.config.EnableServicesElection && !rt.config.EnableLeaderElection {
|
||||
// find all existing local endpoints
|
||||
endpoints, err := rt.getEndpoints(service, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error getting endpoints: %w", rt.provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
// If there were local endpoints deleted
|
||||
if len(endpoints) > 0 {
|
||||
rt.deleteAction(service)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) deleteAction(service *v1.Service) {
|
||||
ClearRoutes(service, rt.instances)
|
||||
}
|
||||
|
||||
func (rt *RoutingTable) setInstanceEndpointsStatus(service *v1.Service, endpoints []string) error {
|
||||
instance := instance.FindServiceInstanceWithTimeout(service, *rt.instances)
|
||||
if instance == nil {
|
||||
log.Error("failed to find the instance", "namespace", service.Namespace, "name", service.Name, "uid", service.UID, "provider", rt.provider.GetLabel())
|
||||
} else {
|
||||
for _, c := range instance.Clusters {
|
||||
for n := range c.Network {
|
||||
// if there are no endpoints set HasEndpoints false just in case
|
||||
if len(endpoints) < 1 {
|
||||
c.Network[n].SetHasEndpoints(false)
|
||||
}
|
||||
// check if endpoint are available and are of same IP family as service
|
||||
if len(endpoints) > 0 && ((net.ParseIP(c.Network[n].IP()).To4() == nil) == (net.ParseIP(endpoints[0]).To4() == nil)) {
|
||||
c.Network[n].SetHasEndpoints(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClearRoutes(service *v1.Service, instances *[]*instance.Instance) []error {
|
||||
errs := []error{}
|
||||
if svcInst := instance.FindServiceInstance(service, *instances); svcInst != nil {
|
||||
clearErrs := ClearRoutesByInstance(service, svcInst, instances)
|
||||
errs = append(errs, clearErrs...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func ClearRoutesByInstance(service *v1.Service, svcInst *instance.Instance, instances *[]*instance.Instance) []error {
|
||||
if svcInst == nil {
|
||||
return []error{fmt.Errorf("failed to remove routes for nil instance of service %s/%s, uid: %s", service.Namespace, service.Name, service.UID)}
|
||||
}
|
||||
errs := []error{}
|
||||
for _, cluster := range svcInst.Clusters {
|
||||
for i := range cluster.Network {
|
||||
route := cluster.Network[i].PrepareRoute()
|
||||
// check if route we are about to delete is not referenced by more than one service
|
||||
if CountRouteReferences(route, instances) <= 1 {
|
||||
err := cluster.Network[i].DeleteRoute()
|
||||
if err != nil && !errors.Is(err, syscall.ESRCH) {
|
||||
log.Error("failed to delete route", "ip", cluster.Network[i].IP(), "err", err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
log.Debug("deleted route", "ip",
|
||||
cluster.Network[i].IP(), "service name", service.Name, "namespace", service.Namespace, "interface", cluster.Network[i].Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func CountRouteReferences(route *netlink.Route, instances *[]*instance.Instance) int {
|
||||
cnt := 0
|
||||
for _, instance := range *instances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for n := range cluster.Network {
|
||||
if cluster.Network[n].HasEndpoints() {
|
||||
r := cluster.Network[n].PrepareRoute()
|
||||
if r.Dst.String() == route.Dst.String() {
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cnt
|
||||
}
|
||||
137
pkg/endpoints/providers/endpoints.go
Normal file
137
pkg/endpoints/providers/endpoints.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
log "log/slog"
|
||||
)
|
||||
|
||||
type Endpoints struct {
|
||||
label string
|
||||
//nolint:staticcheck // SA1019 endpoints are moving to an opt-in only
|
||||
endpoints *v1.Endpoints
|
||||
}
|
||||
|
||||
func NewEndpoints() Provider {
|
||||
return &Endpoints{
|
||||
label: "endpoints",
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *Endpoints) CreateRetryWatcher(ctx context.Context, clientSet *kubernetes.Clientset,
|
||||
service *v1.Service) (*watchtools.RetryWatcher, error) {
|
||||
opts := metav1.ListOptions{
|
||||
FieldSelector: fields.OneTermEqualSelector("metadata.name", service.Name).String(),
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctx, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return clientSet.CoreV1().Endpoints(service.Namespace).Watch(ctx, opts)
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating endpoint watcher: %s", err.Error())
|
||||
}
|
||||
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) LoadObject(endpoints runtime.Object, cancel context.CancelFunc) error {
|
||||
//nolint:staticcheck // SA1019 endpoints have to be explicitly requested now
|
||||
eps, ok := endpoints.(*v1.Endpoints)
|
||||
if !ok {
|
||||
cancel()
|
||||
return fmt.Errorf("[%s] unable to parse Kubernetes services from API watcher", ep.GetLabel())
|
||||
}
|
||||
ep.endpoints = eps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetAllEndpoints() ([]string, error) {
|
||||
result := []string{}
|
||||
for subset := range ep.endpoints.Subsets {
|
||||
for address := range ep.endpoints.Subsets[subset].Addresses {
|
||||
addr := strings.Split(ep.endpoints.Subsets[subset].Addresses[address].IP, "/")
|
||||
result = append(result, addr[0])
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetLocalEndpoints(id string, _ *kubevip.Config) ([]string, error) {
|
||||
var localEndpoints []string
|
||||
|
||||
for _, subset := range ep.endpoints.Subsets {
|
||||
for _, address := range subset.Addresses {
|
||||
log.Debug("processing endpoint", "label", ep.label, "ip", address.IP)
|
||||
|
||||
// 1. Compare the Nodename
|
||||
if address.NodeName != nil && id == *address.NodeName {
|
||||
log.Debug("found local endpoint", "label", ep.label, "ip", address.IP, "hostname", address.Hostname, "nodename", *address.NodeName)
|
||||
localEndpoints = append(localEndpoints, address.IP)
|
||||
continue
|
||||
}
|
||||
// 2. Compare the Hostname (only useful if address.NodeName is not available)
|
||||
if id == address.Hostname {
|
||||
log.Debug("found local endpoint", "label", ep.label, "ip", address.IP, "hostname", address.Hostname)
|
||||
localEndpoints = append(localEndpoints, address.IP)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return localEndpoints, nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) UpdateServiceAnnotation(endpoint string, _ string, service *v1.Service, clientSet *kubernetes.Clientset) error {
|
||||
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
// Retrieve the latest version of Deployment before attempting update
|
||||
// RetryOnConflict uses exponential backoff to avoid exhausting the apiserver
|
||||
currentService, err := clientSet.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentServiceCopy := currentService.DeepCopy()
|
||||
if currentServiceCopy.Annotations == nil {
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
currentServiceCopy.Annotations[kubevip.ActiveEndpoint] = endpoint
|
||||
|
||||
_, err = clientSet.CoreV1().Services(currentService.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
log.Error("error updating Service Spec", "label", ep.GetLabel(), "name", currentServiceCopy.Name, "err", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if retryErr != nil {
|
||||
log.Error("failed to set Services", "label", ep.GetLabel(), "err", retryErr)
|
||||
return retryErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetLabel() string {
|
||||
return ep.label
|
||||
}
|
||||
|
||||
func (ep *Endpoints) GetProtocol() string {
|
||||
return ""
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,17 +13,24 @@ import (
|
||||
"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 endpointslicesProvider struct {
|
||||
type Endpointslices struct {
|
||||
label string
|
||||
endpoints *discoveryv1.EndpointSlice
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) createRetryWatcher(ctx context.Context, sm *Manager,
|
||||
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}}
|
||||
|
||||
@@ -31,9 +38,9 @@ func (ep *endpointslicesProvider) createRetryWatcher(ctx context.Context, sm *Ma
|
||||
LabelSelector: labels.Set(labelSelector.MatchLabels).String(),
|
||||
}
|
||||
|
||||
rw, err := watchtools.NewRetryWatcher("1", &cache.ListWatch{
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctx, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.rwClientSet.DiscoveryV1().EndpointSlices(service.Namespace).Watch(ctx, opts)
|
||||
return clientSet.DiscoveryV1().EndpointSlices(service.Namespace).Watch(ctx, opts)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -43,7 +50,7 @@ func (ep *endpointslicesProvider) createRetryWatcher(ctx context.Context, sm *Ma
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) loadObject(endpoints runtime.Object, cancel context.CancelFunc) error {
|
||||
func (ep *Endpointslices) LoadObject(endpoints runtime.Object, cancel context.CancelFunc) error {
|
||||
eps, ok := endpoints.(*discoveryv1.EndpointSlice)
|
||||
if !ok {
|
||||
cancel()
|
||||
@@ -53,7 +60,7 @@ func (ep *endpointslicesProvider) loadObject(endpoints runtime.Object, cancel co
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getAllEndpoints() ([]string, error) {
|
||||
func (ep *Endpointslices) GetAllEndpoints() ([]string, error) {
|
||||
result := []string{}
|
||||
for _, ep := range ep.endpoints.Endpoints {
|
||||
result = append(result, ep.Addresses...)
|
||||
@@ -61,10 +68,10 @@ func (ep *endpointslicesProvider) getAllEndpoints() ([]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getLocalEndpoints(id string, _ *kubevip.Config) ([]string, error) {
|
||||
func (ep *Endpointslices) GetLocalEndpoints(id string, _ *kubevip.Config) ([]string, error) {
|
||||
var localEndpoints []string
|
||||
for _, endpoint := range ep.endpoints.Endpoints {
|
||||
if !*endpoint.Conditions.Serving {
|
||||
if endpoint.Conditions.Serving == nil || !*endpoint.Conditions.Serving {
|
||||
continue
|
||||
}
|
||||
for _, address := range endpoint.Addresses {
|
||||
@@ -91,11 +98,11 @@ func (ep *endpointslicesProvider) getLocalEndpoints(id string, _ *kubevip.Config
|
||||
return localEndpoints, nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) updateServiceAnnotation(endpoint, endpointIPv6 string, service *v1.Service, sm *Manager) error {
|
||||
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 := sm.clientSet.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})
|
||||
currentService, err := clientSet.CoreV1().Services(service.Namespace).Get(context.TODO(), service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -105,10 +112,10 @@ func (ep *endpointslicesProvider) updateServiceAnnotation(endpoint, endpointIPv6
|
||||
currentServiceCopy.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
currentServiceCopy.Annotations[activeEndpoint] = endpoint
|
||||
currentServiceCopy.Annotations[activeEndpointIPv6] = endpointIPv6
|
||||
currentServiceCopy.Annotations[kubevip.ActiveEndpoint] = endpoint
|
||||
currentServiceCopy.Annotations[kubevip.ActiveEndpointIPv6] = endpointIPv6
|
||||
|
||||
_, err = sm.clientSet.CoreV1().Services(currentService.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
_, 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
|
||||
@@ -123,10 +130,10 @@ func (ep *endpointslicesProvider) updateServiceAnnotation(endpoint, endpointIPv6
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getLabel() string {
|
||||
func (ep *Endpointslices) GetLabel() string {
|
||||
return ep.label
|
||||
}
|
||||
|
||||
func (ep *endpointslicesProvider) getProtocol() string {
|
||||
func (ep *Endpointslices) GetProtocol() string {
|
||||
return string(ep.endpoints.AddressType)
|
||||
}
|
||||
22
pkg/endpoints/providers/interface.go
Normal file
22
pkg/endpoints/providers/interface.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
CreateRetryWatcher(context.Context, *kubernetes.Clientset,
|
||||
*v1.Service) (*watchtools.RetryWatcher, error)
|
||||
GetAllEndpoints() ([]string, error)
|
||||
GetLocalEndpoints(string, *kubevip.Config) ([]string, error)
|
||||
GetLabel() string
|
||||
UpdateServiceAnnotation(string, string, *v1.Service, *kubernetes.Clientset) error
|
||||
LoadObject(runtime.Object, context.CancelFunc) error
|
||||
GetProtocol() string
|
||||
}
|
||||
@@ -221,7 +221,7 @@ func (m *member) tryToBeLeader(ctx context.Context) {
|
||||
|
||||
func (m *member) resignOnCancel(ctx context.Context) {
|
||||
<-ctx.Done()
|
||||
if err := m.election.Resign(m.client.Ctx()); err != nil {
|
||||
if err := m.election.Resign(m.client.Ctx()); err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Error("Failed to resign after the context was canceled", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestRunElectionWithMemberIDCollision(t *testing.T) {
|
||||
Client: cli,
|
||||
},
|
||||
Name: electionName,
|
||||
MemberID: "my-host",
|
||||
MemberID: randomElectionNameForTest("my-host"),
|
||||
LeaseDurationSeconds: 1,
|
||||
Callbacks: etcd.LeaderCallbacks{
|
||||
OnStartedLeading: func(ctx context.Context) {
|
||||
@@ -90,19 +90,21 @@ func TestRunElectionWithTwoMembersAndReelection(t *testing.T) {
|
||||
|
||||
config1 := configBase
|
||||
config1.EtcdConfig.Client = cliMember1
|
||||
config1.MemberID = "my-host"
|
||||
config1.MemberID = randomElectionNameForTest("my-host")
|
||||
uniqueID := rand.Uint64()
|
||||
config1.MemberUniqueID = &uniqueID
|
||||
config1.Callbacks = baseCallbacksForName(config1.MemberID)
|
||||
syncMembers := make(chan (any))
|
||||
config1.Callbacks.OnStartedLeading = func(_ context.Context) {
|
||||
log.Println("I'm my-host, the new leader!!!!")
|
||||
close(syncMembers)
|
||||
log.Println("Losing the leadership on purpose by stopping renewing the lease")
|
||||
g.Expect(cliMember1.Lease.Close()).To(Succeed())
|
||||
log.Println("Member1 leases closed")
|
||||
}
|
||||
|
||||
config2 := configBase
|
||||
config2.MemberID = "my-other-host"
|
||||
config2.MemberID = randomElectionNameForTest("my-other-host")
|
||||
config2.Callbacks = baseCallbacksForName(config2.MemberID)
|
||||
config2.Callbacks.OnStartedLeading = func(_ context.Context) {
|
||||
log.Println("I'm my-other-host, the new leader!!!!")
|
||||
@@ -116,14 +118,14 @@ func TestRunElectionWithTwoMembersAndReelection(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
g.Expect(etcd.RunElection(member1Ctx, &config1)).To(Succeed())
|
||||
log.Println("Member1 routine done")
|
||||
log.Printf("%s routine done\n", config1.MemberID)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(time.Millisecond * 50) // Make sure member1 becomes leader
|
||||
<-syncMembers
|
||||
g.Expect(etcd.RunElection(member2Ctx, &config2)).To(Succeed())
|
||||
log.Println("Member2 routine done")
|
||||
log.Printf("%s routine done\n", config2.MemberID)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
@@ -1,41 +1,34 @@
|
||||
package cluster
|
||||
package instance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"log/slog"
|
||||
log "log/slog"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
||||
"github.com/kube-vip/kube-vip/pkg/sysctl"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
LoadbalancerHostname = "kube-vip.io/loadbalancerHostname"
|
||||
ServiceInterface = "kube-vip.io/serviceInterface"
|
||||
LoadbalancerIPAnnotation = "kube-vip.io/loadbalancerIPs"
|
||||
)
|
||||
|
||||
// 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
|
||||
Clusters []*cluster.Cluster
|
||||
|
||||
// Service uses DHCP
|
||||
IsDHCP bool
|
||||
@@ -58,8 +51,7 @@ type Port struct {
|
||||
}
|
||||
|
||||
func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterface.Manager, arpMgr *arp.Manager) (*Instance, error) {
|
||||
instanceAddresses := FetchServiceAddresses(svc)
|
||||
//instanceUID := string(svc.UID)
|
||||
instanceAddresses, _ := FetchServiceAddresses(svc)
|
||||
|
||||
var newVips []*kubevip.Config
|
||||
var link netlink.Link
|
||||
@@ -68,7 +60,7 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
|
||||
for _, address := range instanceAddresses {
|
||||
// Detect if we're using a specific interface for services
|
||||
var svcInterface string
|
||||
svcInterface = svc.Annotations[ServiceInterface] // If the service has a specific interface defined, then use it
|
||||
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 {
|
||||
@@ -129,7 +121,7 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
|
||||
|
||||
subnet := ""
|
||||
var err error
|
||||
if vip.IsIPv4(address) {
|
||||
if utils.IsIPv4(address) {
|
||||
if ipv4AutoSubnet {
|
||||
subnet, err = autoFindSubnet(link, address)
|
||||
if err != nil {
|
||||
@@ -157,25 +149,24 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
|
||||
}
|
||||
}
|
||||
|
||||
//log.Info("new instance", "svc", *svc, "interface", svcInterface)
|
||||
|
||||
// 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,
|
||||
VIP: address,
|
||||
Interface: svcInterface,
|
||||
SingleNode: true,
|
||||
EnableARP: config.EnableARP,
|
||||
EnableBGP: config.EnableBGP,
|
||||
VIPSubnet: subnet,
|
||||
EnableRoutingTable: config.EnableRoutingTable,
|
||||
RoutingTableID: config.RoutingTableID,
|
||||
RoutingTableType: config.RoutingTableType,
|
||||
RoutingProtocol: config.RoutingProtocol,
|
||||
ArpBroadcastRate: config.ArpBroadcastRate,
|
||||
EnableServiceSecurity: config.EnableServiceSecurity,
|
||||
DNSMode: config.DNSMode,
|
||||
DisableServiceUpdates: config.DisableServiceUpdates,
|
||||
EnableServicesElection: config.EnableServicesElection,
|
||||
PreserveVIPOnLeadershipLoss: config.PreserveVIPOnLeadershipLoss,
|
||||
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
|
||||
EnableLeaderElection: config.EnableLeaderElection,
|
||||
},
|
||||
@@ -196,9 +187,9 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
|
||||
// }
|
||||
|
||||
if svc.Annotations != nil {
|
||||
instance.DHCPInterfaceHwaddr = svc.Annotations[HWAddrKey]
|
||||
instance.DHCPInterfaceIP = svc.Annotations[RequestedIP]
|
||||
instance.DHCPHostname = svc.Annotations[LoadbalancerHostname]
|
||||
instance.DHCPInterfaceHwaddr = svc.Annotations[kubevip.HwAddrKey]
|
||||
instance.DHCPInterfaceIP = svc.Annotations[kubevip.RequestedIP]
|
||||
instance.DHCPHostname = svc.Annotations[kubevip.LoadbalancerHostname]
|
||||
}
|
||||
|
||||
configPorts := make([]kubevip.Port, 0)
|
||||
@@ -241,7 +232,7 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
|
||||
}
|
||||
|
||||
for _, vipConfig := range instance.VIPConfigs {
|
||||
c, err := InitCluster(vipConfig, false, intfMgr, arpMgr)
|
||||
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
|
||||
@@ -253,7 +244,6 @@ func NewInstance(svc *v1.Service, config *kubevip.Config, intfMgr *networkinterf
|
||||
|
||||
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
|
||||
@@ -371,6 +361,28 @@ func (i *Instance) startDHCP() error {
|
||||
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
|
||||
@@ -397,64 +409,108 @@ func (i *Instance) startDHCP() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FetchIngressAddresses tries to get the addresses from status.loadBalancerIP
|
||||
func FetchLoadBalancerIngressAddresses(s *v1.Service) []string {
|
||||
// FetchLoadBalancerIngressAddresses tries to get the addresses from status.loadBalancerIP
|
||||
func FetchLoadBalancerIngress(s *v1.Service) ([]string, []string) {
|
||||
// If the service has no status, return empty
|
||||
lbStatusAddresses := []string{}
|
||||
|
||||
lbStatusHostnames := []string{}
|
||||
if len(s.Status.LoadBalancer.Ingress) == 0 {
|
||||
return lbStatusAddresses
|
||||
return lbStatusAddresses, lbStatusHostnames
|
||||
}
|
||||
|
||||
for _, ingress := range s.Status.LoadBalancer.Ingress {
|
||||
if ingress.IP != "" {
|
||||
lbStatusAddresses = append(lbStatusAddresses, ingress.IP)
|
||||
}
|
||||
// TODO: Handle hostname if needed
|
||||
if ingress.Hostname != "" {
|
||||
lbStatusHostnames = append(lbStatusHostnames, ingress.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
return lbStatusAddresses
|
||||
return lbStatusAddresses, lbStatusHostnames
|
||||
}
|
||||
|
||||
// FetchServiceAddresses tries to get the addresses from annotations
|
||||
// kube-vip.io/loadbalancerIPs, then from spec.loadbalancerIP
|
||||
func FetchServiceAddresses(s *v1.Service) []string {
|
||||
func FetchServiceAddresses(s *v1.Service) ([]string, []string) {
|
||||
annotationAvailable := false
|
||||
if s.Annotations != nil {
|
||||
if v, annotationAvailable := s.Annotations[LoadbalancerIPAnnotation]; annotationAvailable {
|
||||
|
||||
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))
|
||||
var trimmedHostnames []string
|
||||
for _, a := range ips {
|
||||
a = strings.TrimSpace(a)
|
||||
ip := net.ParseIP(a)
|
||||
if ip == nil {
|
||||
// this is probably a DNS name
|
||||
trimmedHostnames = append(trimmedHostnames, a)
|
||||
} else {
|
||||
trimmedIPs = append(trimmedIPs, ip.String())
|
||||
}
|
||||
}
|
||||
return trimmedIPs
|
||||
return trimmedIPs, trimmedHostnames
|
||||
}
|
||||
}
|
||||
|
||||
lbStatusAddresses := []string{}
|
||||
lbStatusHostnames := []string{}
|
||||
if !annotationAvailable {
|
||||
if len(s.Status.LoadBalancer.Ingress) > 0 {
|
||||
for _, ingress := range s.Status.LoadBalancer.Ingress {
|
||||
lbStatusAddresses = append(lbStatusAddresses, ingress.IP)
|
||||
lbStatusAddresses, lbStatusHostnames = FetchLoadBalancerIngress(s)
|
||||
}
|
||||
|
||||
// Spec.LoadBalancerIP legacy handling
|
||||
// if the loadBalancerIP is different from Status.LoadBalancer.Ingress IPs
|
||||
// return the legacy LB as spec wins over status.
|
||||
if lbIP := net.ParseIP(s.Spec.LoadBalancerIP); lbIP != nil && len(lbStatusAddresses) > 0 {
|
||||
isLbIPv4 := utils.IsIPv4(s.Spec.LoadBalancerIP)
|
||||
for _, a := range lbStatusAddresses {
|
||||
if lbStatusIP := net.ParseIP(a); lbStatusIP != nil && utils.IsIPv4(a) == isLbIPv4 && !lbIP.Equal(lbStatusIP) {
|
||||
return []string{s.Spec.LoadBalancerIP}, []string{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 len(lbStatusAddresses) > 0 || len(lbStatusHostnames) > 0 {
|
||||
return lbStatusAddresses, lbStatusHostnames
|
||||
}
|
||||
|
||||
if s.Spec.LoadBalancerIP != "" {
|
||||
return []string{s.Spec.LoadBalancerIP}
|
||||
return []string{s.Spec.LoadBalancerIP}, []string{}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
return []string{}, []string{}
|
||||
}
|
||||
|
||||
func FindServiceInstance(svc *v1.Service, instances []*Instance) *Instance {
|
||||
log.Debug("finding service", "namespace", svc.Namespace, "name", svc.Name, "UID", svc.UID)
|
||||
for i := range instances {
|
||||
log.Debug("saved service", "instance", i, "UID", instances[i].ServiceSnapshot.UID)
|
||||
if instances[i].ServiceSnapshot.UID == svc.UID {
|
||||
return instances[i]
|
||||
}
|
||||
}
|
||||
log.Debug("instance not found", "namespace", svc.Namespace, "name", svc.Name, "UID", svc.UID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindServiceInstanceWithTimeout(svc *v1.Service, instances []*Instance) *Instance {
|
||||
log.Debug("finding service with timeout", "namespace", svc.Namespace, "name", svc.Name, "UID", svc.UID)
|
||||
ticker := time.NewTicker(time.Millisecond * 200)
|
||||
defer ticker.Stop()
|
||||
to := time.NewTimer(time.Second * 60)
|
||||
defer to.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-to.C:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
for i := range instances {
|
||||
log.Debug("saved service", "instance", i, "UID", instances[i].ServiceSnapshot.UID)
|
||||
if instances[i].ServiceSnapshot.UID == svc.UID {
|
||||
return instances[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
pkg/kubevip/annotations.go
Normal file
63
pkg/kubevip/annotations.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package kubevip
|
||||
|
||||
const (
|
||||
// Hardware address of the host that has the VIP
|
||||
HwAddrKey = "kube-vip.io/hwaddr"
|
||||
|
||||
// The IP address that is requested
|
||||
RequestedIP = "kube-vip.io/requestedIP"
|
||||
|
||||
// The host that has the VIP
|
||||
VipHost = "kube-vip.io/vipHost"
|
||||
|
||||
// Enable Egress on a service
|
||||
Egress = "kube-vip.io/egress"
|
||||
|
||||
// Enable internal Egress
|
||||
EgressInternal = "kube-vip.io/egress-internal"
|
||||
|
||||
// Egress should be IPv6
|
||||
EgressIPv6 = "kube-vip.io/egress-ipv6"
|
||||
|
||||
// Ports that traffic is allowed to access from the egress VIP
|
||||
EgressDestinationPorts = "kube-vip.io/egress-destination-ports"
|
||||
|
||||
// Allowed incoming ports to the VIP
|
||||
EgressSourcePorts = "kube-vip.io/egress-source-ports"
|
||||
|
||||
// Allowed networks for the Egress to be enabled for
|
||||
EgressAllowedNetworks = "kube-vip.io/egress-allowed-networks"
|
||||
|
||||
// Networks that we wont Egress for
|
||||
EgressDeniedNetworks = "kube-vip.io/egress-denied-networks"
|
||||
|
||||
// The current active endpoint(pod) for the Egress VIP
|
||||
ActiveEndpoint = "kube-vip.io/active-endpoint"
|
||||
|
||||
// The current active endpoint(pod) for the Egress VIP (v6)
|
||||
ActiveEndpointIPv6 = "kube-vip.io/active-endpoint-ipv6"
|
||||
|
||||
// Flush the conntrack rules (remove existing sessions) once Egress is configured
|
||||
FlushContrack = "kube-vip.io/flush-conntrack"
|
||||
|
||||
// Configure LoadBalancer IPs instead of relying on a controller
|
||||
LoadbalancerIPAnnotation = "kube-vip.io/loadbalancerIPs"
|
||||
|
||||
// Ignore the LoadBalancer Service
|
||||
LoadbalancerIgnore = "kube-vip.io/ignore"
|
||||
|
||||
// Used to configure DHCP with a Hostname
|
||||
LoadbalancerHostname = "kube-vip.io/loadbalancerHostname"
|
||||
|
||||
// Define an interface name to bind the address of the LoadBalancer to
|
||||
ServiceInterface = "kube-vip.io/serviceInterface"
|
||||
|
||||
ServiceSecurityIgnore = "kube-vip.io/ignore-service-security"
|
||||
|
||||
// Enable UPNP on a Service
|
||||
UpnpEnabled = "kube-vip.io/forwardUPNP"
|
||||
|
||||
RPFilter = "kube-vip.io/rp_filter" // Set the return path filter for a specific service interface
|
||||
|
||||
ServiceLease = "kube-vip.io/leaseName"
|
||||
)
|
||||
234
pkg/kubevip/config_bgp.go
Normal file
234
pkg/kubevip/config_bgp.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package kubevip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
api "github.com/osrg/gobgp/v3/api"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// Peer defines a BGP Peer
|
||||
type BGPPeer struct {
|
||||
Address string
|
||||
Port uint16
|
||||
AS uint32
|
||||
Password string
|
||||
MultiHop bool
|
||||
MpbgpNexthop string
|
||||
MpbgpIPv4 string
|
||||
MpbgpIPv6 string
|
||||
}
|
||||
|
||||
// Config defines the BGP server configuration
|
||||
type BGPConfig struct {
|
||||
AS uint32
|
||||
RouterID string
|
||||
SourceIP string
|
||||
SourceIF string
|
||||
MpbgpNexthop string
|
||||
MpbgpIPv4 string
|
||||
MpbgpIPv6 string
|
||||
|
||||
HoldTime uint64
|
||||
KeepaliveInterval uint64
|
||||
|
||||
Peers []BGPPeer
|
||||
|
||||
Zebra ZebraConfig
|
||||
}
|
||||
|
||||
// Defines Zebra connection configuration. More on the topic - https://github.com/osrg/gobgp/blob/master/docs/sources/zebra.md#configuration
|
||||
type ZebraConfig struct {
|
||||
Enabled bool
|
||||
URL string
|
||||
Version uint32
|
||||
SoftwareName string
|
||||
}
|
||||
|
||||
// ParseBGPPeerConfig - take a string and parses it into an array of peers
|
||||
func ParseBGPPeerConfig(config string) (bgpPeers []BGPPeer, err error) {
|
||||
peers := strings.Split(config, ",")
|
||||
if len(peers) == 0 {
|
||||
return nil, fmt.Errorf("no BGP Peer configurations found")
|
||||
}
|
||||
|
||||
for x := range peers {
|
||||
peerStr := peers[x]
|
||||
config := strings.Split(peerStr, "/")
|
||||
peerStr = config[0]
|
||||
if peerStr == "" {
|
||||
continue
|
||||
}
|
||||
isV6Peer := peerStr[0] == '['
|
||||
|
||||
address := ""
|
||||
if isV6Peer {
|
||||
addressEndPos := strings.IndexByte(peerStr, ']')
|
||||
if addressEndPos == -1 {
|
||||
return nil, fmt.Errorf("no matching ] found for IPv6 BGP Peer")
|
||||
}
|
||||
address = peerStr[1:addressEndPos]
|
||||
peerStr = peerStr[addressEndPos+1:]
|
||||
}
|
||||
|
||||
peer := strings.Split(peerStr, ":")
|
||||
if len(peer) < 2 {
|
||||
return nil, fmt.Errorf("mandatory peering params <host>:<AS> incomplete")
|
||||
}
|
||||
|
||||
if !isV6Peer {
|
||||
address = peer[0]
|
||||
}
|
||||
|
||||
ASNumber, err := strconv.ParseUint(peer[1], 10, 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP Peer AS format error [%s]", peer[1])
|
||||
}
|
||||
|
||||
password := ""
|
||||
if len(peer) >= 3 {
|
||||
password = peer[2]
|
||||
}
|
||||
|
||||
multiHop := false
|
||||
if len(peer) >= 4 {
|
||||
multiHop, err = strconv.ParseBool(peer[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP MultiHop format error (true/false) [%s]", peer[1])
|
||||
}
|
||||
}
|
||||
|
||||
var port uint64
|
||||
if len(peer) >= 5 {
|
||||
port, err = strconv.ParseUint(peer[4], 10, 16)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("BGP Peer AS format error [%s]", peer[1])
|
||||
}
|
||||
} else {
|
||||
port = 179
|
||||
}
|
||||
|
||||
var mpbgpNexthop, mpbgpIPv4, mpbgpIPv6 string
|
||||
|
||||
if len(config) > 1 {
|
||||
configData := strings.Split(config[1], ";")
|
||||
for _, cfg := range configData {
|
||||
c := strings.Split(cfg, "=")
|
||||
switch c[0] {
|
||||
case "mpbgp_nexthop":
|
||||
mpbgpNexthop = c[1]
|
||||
case "mpbgp_ipv4":
|
||||
mpbgpIPv4 = c[1]
|
||||
case "mpbgp_ipv6":
|
||||
mpbgpIPv6 = c[1]
|
||||
default:
|
||||
return nil, fmt.Errorf("peer configuration parameter '%s' is not supported", c[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peerConfig := BGPPeer{
|
||||
Address: address,
|
||||
AS: uint32(ASNumber),
|
||||
Port: uint16(port),
|
||||
Password: password,
|
||||
MultiHop: multiHop,
|
||||
MpbgpNexthop: mpbgpNexthop,
|
||||
MpbgpIPv4: mpbgpIPv4,
|
||||
MpbgpIPv6: mpbgpIPv6,
|
||||
}
|
||||
|
||||
bgpPeers = append(bgpPeers, peerConfig)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *BGPPeer) FindMpbgpAddresses(ap *api.Peer, server *BGPConfig) (string, string, error) {
|
||||
var ipv4Address, ipv6Address string
|
||||
switch p.MpbgpNexthop {
|
||||
case "fixed":
|
||||
ap.Transport.LocalAddress = server.SourceIP
|
||||
if p.MpbgpIPv4 == "" && p.MpbgpIPv6 == "" {
|
||||
return "", "", fmt.Errorf("to use MP-BGP with fixed address at least one IPv4 or IPv6 address has to be provided [current - IPv4: %s, IPv6: %s]",
|
||||
p.MpbgpIPv4, p.MpbgpIPv6)
|
||||
}
|
||||
|
||||
if p.MpbgpIPv4 != "" {
|
||||
if net.ParseIP(p.MpbgpIPv4) == nil {
|
||||
return "", "", fmt.Errorf("provided address '%s' is not a valid IPv4 address", p.MpbgpIPv4)
|
||||
}
|
||||
}
|
||||
if p.MpbgpIPv6 != "" {
|
||||
if net.ParseIP(p.MpbgpIPv6) == nil {
|
||||
return "", "", fmt.Errorf("provided address '%s' is not a valid IPv6 address", p.MpbgpIPv6)
|
||||
}
|
||||
}
|
||||
|
||||
ipv4Address = p.MpbgpIPv4
|
||||
ipv6Address = p.MpbgpIPv6
|
||||
case "auto_sourceip":
|
||||
ap.Transport.LocalAddress = server.SourceIP
|
||||
|
||||
// Resolve the local interface by SourceIP
|
||||
iface, err := utils.GetInterfaceByIP(server.SourceIP)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get interface by IP: %v", err)
|
||||
}
|
||||
|
||||
if utils.IsIPv4(server.SourceIP) {
|
||||
// Get the non link-local IPv6 address on that interface
|
||||
ipv6Address, err = utils.GetNonLinkLocalIP(iface, netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv6 address: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Get the non link-local IPv4 address on that interface
|
||||
ipv4Address, err = utils.GetNonLinkLocalIP(iface, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv4 address: %v", err)
|
||||
}
|
||||
}
|
||||
case "auto_sourceif":
|
||||
ap.Transport.BindInterface = server.SourceIF
|
||||
|
||||
iface, err := netlink.LinkByName(server.SourceIF)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get interface by name: %v", err)
|
||||
}
|
||||
|
||||
// Get the non link-local IPv4 address on that interface
|
||||
ipv4Address, err = utils.GetNonLinkLocalIP(&iface, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv4 address: %v", err)
|
||||
}
|
||||
|
||||
// Get the non link-local IPv6 address on that interface
|
||||
ipv6Address, err = utils.GetNonLinkLocalIP(&iface, netlink.FAMILY_V6)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get non link-local IPv6 address: %v", err)
|
||||
}
|
||||
default:
|
||||
return "", "", fmt.Errorf("option %s for MP-BPG nexthop is not supported", server.MpbgpNexthop)
|
||||
}
|
||||
|
||||
return ipv4Address, ipv6Address, nil
|
||||
}
|
||||
|
||||
func (p *BGPPeer) SetMpbgpOptions(server *BGPConfig) {
|
||||
if p.MpbgpNexthop == "" {
|
||||
p.MpbgpNexthop = server.MpbgpNexthop
|
||||
}
|
||||
|
||||
if p.MpbgpIPv4 == "" {
|
||||
p.MpbgpIPv4 = server.MpbgpIPv4
|
||||
}
|
||||
|
||||
if p.MpbgpIPv6 == "" {
|
||||
p.MpbgpIPv6 = server.MpbgpIPv6
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"math"
|
||||
"math/bits"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/detector"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// ParseEnvironment - will popultate the configuration from environment variables
|
||||
@@ -289,6 +291,21 @@ func ParseEnvironment(c *Config) error {
|
||||
c.ArpBroadcastRate = 3000
|
||||
}
|
||||
|
||||
// Determine if VIP should be preserved on leadership loss
|
||||
// true: VIP addresses remain on interface, only ARP/NDP broadcasting stops
|
||||
// false (default): VIP addresses are deleted on leadership loss (legacy behavior)
|
||||
env = os.Getenv(vipPreserveOnLeadershipLoss)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.PreserveVIPOnLeadershipLoss = b
|
||||
} else {
|
||||
// Default to false for backward compatibility
|
||||
c.PreserveVIPOnLeadershipLoss = false
|
||||
}
|
||||
|
||||
// Wireguard Mode
|
||||
env = os.Getenv(vipWireguard)
|
||||
if env != "" {
|
||||
@@ -421,7 +438,7 @@ func ParseEnvironment(c *Config) error {
|
||||
// Peer AS
|
||||
env = os.Getenv(bgpPeers)
|
||||
if env != "" {
|
||||
peers, err := bgp.ParseBGPPeerConfig(env)
|
||||
peers, err := ParseBGPPeerConfig(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -500,6 +517,34 @@ func ParseEnvironment(c *Config) error {
|
||||
c.BGPConfig.KeepaliveInterval = u64
|
||||
}
|
||||
|
||||
env = os.Getenv(zebraEnable)
|
||||
if env != "" {
|
||||
result, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.BGPConfig.Zebra.Enabled = result
|
||||
}
|
||||
|
||||
env = os.Getenv(zebraURL)
|
||||
if env != "" {
|
||||
c.BGPConfig.Zebra.URL = env
|
||||
}
|
||||
|
||||
env = os.Getenv(zebraVersion)
|
||||
if 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
|
||||
env = os.Getenv(lbEnable)
|
||||
if env != "" {
|
||||
@@ -578,13 +623,13 @@ 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)
|
||||
@@ -618,5 +663,305 @@ func ParseEnvironment(c *Config) error {
|
||||
c.HealthCheckPort = int(i)
|
||||
}
|
||||
|
||||
env = os.Getenv(enableUPNP)
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.EnableUPNP = b
|
||||
}
|
||||
|
||||
if env = os.Getenv(egressClean); env == "" {
|
||||
env = os.Getenv(strings.ToUpper(egressClean))
|
||||
}
|
||||
if env != "" {
|
||||
b, err := strconv.ParseBool(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.EgressClean = b
|
||||
}
|
||||
|
||||
// check for configuration file path
|
||||
env = os.Getenv(configFile)
|
||||
if env != "" {
|
||||
c.ConfigFile = env
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfigFromFile loads configuration from a JSON or YAML file
|
||||
func LoadConfigFromFile(configFilePath string) (*Config, error) {
|
||||
if configFilePath == "" {
|
||||
return nil, fmt.Errorf("config file path is empty")
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("config file does not exist: %s", configFilePath)
|
||||
}
|
||||
|
||||
// Read file content
|
||||
data, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file %s: %v", configFilePath, err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
ext := strings.ToLower(filepath.Ext(configFilePath))
|
||||
|
||||
switch ext {
|
||||
case ".json":
|
||||
err = json.Unmarshal(data, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON config file %s: %v", configFilePath, err)
|
||||
}
|
||||
case ".yaml", ".yml":
|
||||
err = yaml.Unmarshal(data, &config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse YAML config file %s: %v", configFilePath, err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported config file format %s. Supported formats: .json, .yaml, .yml", ext)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// MergeConfigFromFile merges configuration loaded from file with existing config
|
||||
// Priority: command line flags > environment variables > config file
|
||||
func MergeConfigFromFile(c *Config, configFilePath string) error {
|
||||
if configFilePath == "" {
|
||||
return nil // No config file specified, nothing to merge
|
||||
}
|
||||
|
||||
fileConfig, err := LoadConfigFromFile(configFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Merge file config with existing config
|
||||
// Only set values from file if they haven't been set by flags or env vars
|
||||
mergeConfigValues(c, fileConfig)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeConfigValues merges values from fileConfig into baseConfig
|
||||
// Only overwrites zero values in baseConfig
|
||||
func mergeConfigValues(baseConfig, fileConfig *Config) {
|
||||
// Basic configuration
|
||||
if baseConfig.Logging == 0 && fileConfig.Logging != 0 {
|
||||
baseConfig.Logging = fileConfig.Logging
|
||||
}
|
||||
|
||||
// Network configuration
|
||||
if baseConfig.Interface == "" && fileConfig.Interface != "" {
|
||||
baseConfig.Interface = fileConfig.Interface
|
||||
}
|
||||
if baseConfig.ServicesInterface == "" && fileConfig.ServicesInterface != "" {
|
||||
baseConfig.ServicesInterface = fileConfig.ServicesInterface
|
||||
}
|
||||
if baseConfig.VIP == "" && fileConfig.VIP != "" {
|
||||
baseConfig.VIP = fileConfig.VIP
|
||||
}
|
||||
if baseConfig.VIPSubnet == "" && fileConfig.VIPSubnet != "" {
|
||||
baseConfig.VIPSubnet = fileConfig.VIPSubnet
|
||||
}
|
||||
if baseConfig.Address == "" && fileConfig.Address != "" {
|
||||
baseConfig.Address = fileConfig.Address
|
||||
}
|
||||
if baseConfig.Port == 0 && fileConfig.Port != 0 {
|
||||
baseConfig.Port = fileConfig.Port
|
||||
}
|
||||
if baseConfig.NodeName == "" && fileConfig.NodeName != "" {
|
||||
baseConfig.NodeName = fileConfig.NodeName
|
||||
}
|
||||
|
||||
// Boolean flags - only merge if not explicitly set
|
||||
if !baseConfig.EnableARP && fileConfig.EnableARP {
|
||||
baseConfig.EnableARP = fileConfig.EnableARP
|
||||
}
|
||||
if !baseConfig.EnableBGP && fileConfig.EnableBGP {
|
||||
baseConfig.EnableBGP = fileConfig.EnableBGP
|
||||
}
|
||||
if !baseConfig.EnableWireguard && fileConfig.EnableWireguard {
|
||||
baseConfig.EnableWireguard = fileConfig.EnableWireguard
|
||||
}
|
||||
if !baseConfig.EnableRoutingTable && fileConfig.EnableRoutingTable {
|
||||
baseConfig.EnableRoutingTable = fileConfig.EnableRoutingTable
|
||||
}
|
||||
if !baseConfig.EnableControlPlane && fileConfig.EnableControlPlane {
|
||||
baseConfig.EnableControlPlane = fileConfig.EnableControlPlane
|
||||
}
|
||||
if !baseConfig.DetectControlPlane && fileConfig.DetectControlPlane {
|
||||
baseConfig.DetectControlPlane = fileConfig.DetectControlPlane
|
||||
}
|
||||
if !baseConfig.EnableServices && fileConfig.EnableServices {
|
||||
baseConfig.EnableServices = fileConfig.EnableServices
|
||||
}
|
||||
if !baseConfig.EnableServicesElection && fileConfig.EnableServicesElection {
|
||||
baseConfig.EnableServicesElection = fileConfig.EnableServicesElection
|
||||
}
|
||||
if !baseConfig.EnableNodeLabeling && fileConfig.EnableNodeLabeling {
|
||||
baseConfig.EnableNodeLabeling = fileConfig.EnableNodeLabeling
|
||||
}
|
||||
if !baseConfig.EnableLoadBalancer && fileConfig.EnableLoadBalancer {
|
||||
baseConfig.EnableLoadBalancer = fileConfig.EnableLoadBalancer
|
||||
}
|
||||
if !baseConfig.DDNS && fileConfig.DDNS {
|
||||
baseConfig.DDNS = fileConfig.DDNS
|
||||
}
|
||||
if !baseConfig.SingleNode && fileConfig.SingleNode {
|
||||
baseConfig.SingleNode = fileConfig.SingleNode
|
||||
}
|
||||
if !baseConfig.StartAsLeader && fileConfig.StartAsLeader {
|
||||
baseConfig.StartAsLeader = fileConfig.StartAsLeader
|
||||
}
|
||||
if !baseConfig.PreserveVIPOnLeadershipLoss && fileConfig.PreserveVIPOnLeadershipLoss {
|
||||
baseConfig.PreserveVIPOnLeadershipLoss = fileConfig.PreserveVIPOnLeadershipLoss
|
||||
}
|
||||
|
||||
// Service configuration
|
||||
if baseConfig.Namespace == "" && fileConfig.Namespace != "" {
|
||||
baseConfig.Namespace = fileConfig.Namespace
|
||||
}
|
||||
if baseConfig.ServiceNamespace == "" && fileConfig.ServiceNamespace != "" {
|
||||
baseConfig.ServiceNamespace = fileConfig.ServiceNamespace
|
||||
}
|
||||
if baseConfig.ServicesLeaseName == "" && fileConfig.ServicesLeaseName != "" {
|
||||
baseConfig.ServicesLeaseName = fileConfig.ServicesLeaseName
|
||||
}
|
||||
|
||||
// LoadBalancer configuration
|
||||
if baseConfig.LoadBalancerPort == 0 && fileConfig.LoadBalancerPort != 0 {
|
||||
baseConfig.LoadBalancerPort = fileConfig.LoadBalancerPort
|
||||
}
|
||||
if baseConfig.LoadBalancerForwardingMethod == "" && fileConfig.LoadBalancerForwardingMethod != "" {
|
||||
baseConfig.LoadBalancerForwardingMethod = fileConfig.LoadBalancerForwardingMethod
|
||||
}
|
||||
if baseConfig.LoadBalancerClassName == "" && fileConfig.LoadBalancerClassName != "" {
|
||||
baseConfig.LoadBalancerClassName = fileConfig.LoadBalancerClassName
|
||||
}
|
||||
|
||||
// Routing Table configuration
|
||||
if baseConfig.RoutingTableID == 0 && fileConfig.RoutingTableID != 0 {
|
||||
baseConfig.RoutingTableID = fileConfig.RoutingTableID
|
||||
}
|
||||
if baseConfig.RoutingTableType == 0 && fileConfig.RoutingTableType != 0 {
|
||||
baseConfig.RoutingTableType = fileConfig.RoutingTableType
|
||||
}
|
||||
if baseConfig.RoutingProtocol == 0 && fileConfig.RoutingProtocol != 0 {
|
||||
baseConfig.RoutingProtocol = fileConfig.RoutingProtocol
|
||||
}
|
||||
|
||||
// BGP configuration
|
||||
mergeBGPConfig(&baseConfig.BGPConfig, &fileConfig.BGPConfig)
|
||||
|
||||
// Kubernetes configuration
|
||||
if baseConfig.K8sConfigFile == "" && fileConfig.K8sConfigFile != "" {
|
||||
baseConfig.K8sConfigFile = fileConfig.K8sConfigFile
|
||||
}
|
||||
|
||||
// Leader Election configuration
|
||||
mergeLeaderElectionConfig(&baseConfig.KubernetesLeaderElection, &fileConfig.KubernetesLeaderElection)
|
||||
|
||||
// Prometheus configuration
|
||||
if baseConfig.PrometheusHTTPServer == "" && fileConfig.PrometheusHTTPServer != "" {
|
||||
baseConfig.PrometheusHTTPServer = fileConfig.PrometheusHTTPServer
|
||||
}
|
||||
|
||||
// DNS configuration
|
||||
if baseConfig.DNSMode == "" && fileConfig.DNSMode != "" {
|
||||
baseConfig.DNSMode = fileConfig.DNSMode
|
||||
}
|
||||
|
||||
// Health check configuration
|
||||
if baseConfig.HealthCheckPort == 0 && fileConfig.HealthCheckPort != 0 {
|
||||
baseConfig.HealthCheckPort = fileConfig.HealthCheckPort
|
||||
}
|
||||
|
||||
// Egress configuration
|
||||
if baseConfig.EgressPodCidr == "" && fileConfig.EgressPodCidr != "" {
|
||||
baseConfig.EgressPodCidr = fileConfig.EgressPodCidr
|
||||
}
|
||||
if baseConfig.EgressServiceCidr == "" && fileConfig.EgressServiceCidr != "" {
|
||||
baseConfig.EgressServiceCidr = fileConfig.EgressServiceCidr
|
||||
}
|
||||
|
||||
// Mirror configuration
|
||||
if baseConfig.MirrorDestInterface == "" && fileConfig.MirrorDestInterface != "" {
|
||||
baseConfig.MirrorDestInterface = fileConfig.MirrorDestInterface
|
||||
}
|
||||
|
||||
// Iptables configuration
|
||||
if baseConfig.IptablesBackend == "" && fileConfig.IptablesBackend != "" {
|
||||
baseConfig.IptablesBackend = fileConfig.IptablesBackend
|
||||
}
|
||||
|
||||
// Backend health check interval
|
||||
if baseConfig.BackendHealthCheckInterval == 0 && fileConfig.BackendHealthCheckInterval != 0 {
|
||||
baseConfig.BackendHealthCheckInterval = fileConfig.BackendHealthCheckInterval
|
||||
}
|
||||
|
||||
// ARP broadcast rate
|
||||
if baseConfig.ArpBroadcastRate == 0 && fileConfig.ArpBroadcastRate != 0 {
|
||||
baseConfig.ArpBroadcastRate = fileConfig.ArpBroadcastRate
|
||||
}
|
||||
|
||||
// Annotations
|
||||
if baseConfig.Annotations == "" && fileConfig.Annotations != "" {
|
||||
baseConfig.Annotations = fileConfig.Annotations
|
||||
}
|
||||
|
||||
// Load balancers slice
|
||||
if len(baseConfig.LoadBalancers) == 0 && len(fileConfig.LoadBalancers) > 0 {
|
||||
baseConfig.LoadBalancers = fileConfig.LoadBalancers
|
||||
}
|
||||
}
|
||||
|
||||
// mergeBGPConfig merges BGP configuration
|
||||
func mergeBGPConfig(base, file *BGPConfig) {
|
||||
if base.RouterID == "" && file.RouterID != "" {
|
||||
base.RouterID = file.RouterID
|
||||
}
|
||||
if base.AS == 0 && file.AS != 0 {
|
||||
base.AS = file.AS
|
||||
}
|
||||
if base.SourceIF == "" && file.SourceIF != "" {
|
||||
base.SourceIF = file.SourceIF
|
||||
}
|
||||
if base.SourceIP == "" && file.SourceIP != "" {
|
||||
base.SourceIP = file.SourceIP
|
||||
}
|
||||
if base.HoldTime == 0 && file.HoldTime != 0 {
|
||||
base.HoldTime = file.HoldTime
|
||||
}
|
||||
if base.KeepaliveInterval == 0 && file.KeepaliveInterval != 0 {
|
||||
base.KeepaliveInterval = file.KeepaliveInterval
|
||||
}
|
||||
if len(base.Peers) == 0 && len(file.Peers) > 0 {
|
||||
base.Peers = file.Peers
|
||||
}
|
||||
}
|
||||
|
||||
// mergeLeaderElectionConfig merges leader election configuration
|
||||
func mergeLeaderElectionConfig(base, file *KubernetesLeaderElection) {
|
||||
if base.LeaseName == "" && file.LeaseName != "" {
|
||||
base.LeaseName = file.LeaseName
|
||||
}
|
||||
if base.LeaseDuration == 0 && file.LeaseDuration != 0 {
|
||||
base.LeaseDuration = file.LeaseDuration
|
||||
}
|
||||
if base.RenewDeadline == 0 && file.RenewDeadline != 0 {
|
||||
base.RenewDeadline = file.RenewDeadline
|
||||
}
|
||||
if base.RetryPeriod == 0 && file.RetryPeriod != 0 {
|
||||
base.RetryPeriod = file.RetryPeriod
|
||||
}
|
||||
if len(base.LeaseAnnotations) == 0 && len(file.LeaseAnnotations) > 0 {
|
||||
base.LeaseAnnotations = file.LeaseAnnotations
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ const (
|
||||
// vip_arpRate - defines the rate of gARP broadcasts
|
||||
vipArpRate = "vip_arpRate"
|
||||
|
||||
// vipPreserveOnLeadershipLoss - if true, VIP addresses will remain on interface when leadership is lost
|
||||
vipPreserveOnLeadershipLoss = "vip_preserve_on_leadership_loss"
|
||||
|
||||
// vipLeaderElection - defines if the kubernetes algorithm should be used
|
||||
vipLeaderElection = "vip_leaderelection"
|
||||
|
||||
@@ -110,6 +113,15 @@ 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
|
||||
@@ -203,8 +215,8 @@ 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.
|
||||
@@ -219,4 +231,13 @@ const (
|
||||
|
||||
// healthCheckPort, if set to non-zero will be the port the health check will listen on
|
||||
healthCheckPort = "health_check_port"
|
||||
|
||||
// enableUPNP enables UPNP functions
|
||||
enableUPNP = "enable_upnp"
|
||||
|
||||
// egressClean enables egress cleaning on kube-vip's start
|
||||
egressClean = "egress_clean"
|
||||
|
||||
// configFile defines the path to a JSON/YAML configuration file
|
||||
configFile = "config_file"
|
||||
)
|
||||
|
||||
559
pkg/kubevip/config_file_test.go
Normal file
559
pkg/kubevip/config_file_test.go
Normal file
@@ -0,0 +1,559 @@
|
||||
package kubevip
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfigFromFile(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tmpDir, err := os.MkdirTemp("", "kube-vip-config-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
content string
|
||||
expectedConfig *Config
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "Valid YAML config",
|
||||
filename: "config.yaml",
|
||||
content: `
|
||||
logging: 2
|
||||
enableARP: true
|
||||
enableControlPlane: true
|
||||
enableServices: true
|
||||
address: "192.168.1.100"
|
||||
port: 6443
|
||||
interface: "eth0"
|
||||
namespace: "kube-system"
|
||||
vipSubnet: "192.168.1.0/24"
|
||||
leaseName: "test-lease"
|
||||
leaseDuration: 15
|
||||
renewDeadline: 10
|
||||
retryPeriod: 2
|
||||
prometheusHTTPServer: ":2112"
|
||||
`,
|
||||
expectedConfig: &Config{
|
||||
Logging: 2,
|
||||
EnableARP: true,
|
||||
EnableControlPlane: true,
|
||||
EnableServices: true,
|
||||
Address: "192.168.1.100",
|
||||
Port: 6443,
|
||||
Interface: "eth0",
|
||||
Namespace: "kube-system",
|
||||
VIPSubnet: "192.168.1.0/24",
|
||||
PrometheusHTTPServer: ":2112",
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "test-lease",
|
||||
LeaseDuration: 15,
|
||||
RenewDeadline: 10,
|
||||
RetryPeriod: 2,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid JSON config",
|
||||
filename: "config.json",
|
||||
content: `{
|
||||
"logging": 3,
|
||||
"enableBGP": true,
|
||||
"enableServices": true,
|
||||
"address": "10.0.0.100",
|
||||
"port": 8443,
|
||||
"interface": "ens192",
|
||||
"namespace": "kube-system",
|
||||
"loadBalancers": [
|
||||
{
|
||||
"name": "control-plane",
|
||||
"ports": [
|
||||
{
|
||||
"type": "TCP",
|
||||
"port": 6443
|
||||
}
|
||||
],
|
||||
"bindToVip": true,
|
||||
"forwardingMethod": "local"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
expectedConfig: &Config{
|
||||
Logging: 3,
|
||||
EnableBGP: true,
|
||||
EnableServices: true,
|
||||
Address: "10.0.0.100",
|
||||
Port: 8443,
|
||||
Interface: "ens192",
|
||||
Namespace: "kube-system",
|
||||
LoadBalancers: []LoadBalancer{
|
||||
{
|
||||
Name: "control-plane",
|
||||
Ports: []Port{
|
||||
{
|
||||
Type: "TCP",
|
||||
Port: 6443,
|
||||
},
|
||||
},
|
||||
BindToVip: true,
|
||||
ForwardingMethod: "local",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Complex BGP config",
|
||||
filename: "bgp-config.yaml",
|
||||
content: `
|
||||
enableBGP: true
|
||||
bgpConfig:
|
||||
routerID: "192.168.1.1"
|
||||
as: 65000
|
||||
sourceIF: "eth0"
|
||||
holdTime: 60
|
||||
keepaliveInterval: 20
|
||||
peers:
|
||||
- address: "192.168.1.2"
|
||||
as: 65001
|
||||
port: 179
|
||||
multiHop: false
|
||||
- address: "192.168.1.3"
|
||||
as: 65002
|
||||
port: 179
|
||||
multiHop: true
|
||||
`,
|
||||
expectedConfig: &Config{
|
||||
EnableBGP: true,
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "192.168.1.1",
|
||||
AS: 65000,
|
||||
SourceIF: "eth0",
|
||||
HoldTime: 60,
|
||||
KeepaliveInterval: 20,
|
||||
Peers: []BGPPeer{
|
||||
{
|
||||
Address: "192.168.1.2",
|
||||
AS: 65001,
|
||||
Port: 179,
|
||||
MultiHop: false,
|
||||
},
|
||||
{
|
||||
Address: "192.168.1.3",
|
||||
AS: 65002,
|
||||
Port: 179,
|
||||
MultiHop: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
filename: "invalid.json",
|
||||
content: `{"logging": 2, "invalid": }`,
|
||||
wantErr: true,
|
||||
errContains: "failed to parse JSON config file",
|
||||
},
|
||||
{
|
||||
name: "Invalid YAML",
|
||||
filename: "invalid.yaml",
|
||||
content: "logging: 2\ninvalid: [unclosed",
|
||||
wantErr: true,
|
||||
errContains: "failed to parse YAML config file",
|
||||
},
|
||||
{
|
||||
name: "Unsupported format",
|
||||
filename: "config.txt",
|
||||
content: "logging=2",
|
||||
wantErr: true,
|
||||
errContains: "unsupported config file format",
|
||||
},
|
||||
{
|
||||
name: "Empty path",
|
||||
filename: "",
|
||||
content: "",
|
||||
wantErr: true,
|
||||
errContains: "config file path is empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var filePath string
|
||||
if tt.filename != "" {
|
||||
filePath = filepath.Join(tmpDir, tt.filename)
|
||||
if err := os.WriteFile(filePath, []byte(tt.content), 0600); err != nil {
|
||||
t.Fatalf("Failed to write test file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
config, err := LoadConfigFromFile(filePath)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("LoadConfigFromFile() expected error, got nil")
|
||||
return
|
||||
}
|
||||
if tt.errContains != "" && !containsString(err.Error(), tt.errContains) {
|
||||
t.Errorf("LoadConfigFromFile() error = %v, expected to contain %v", err, tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("LoadConfigFromFile() unexpected error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
t.Errorf("LoadConfigFromFile() returned nil config")
|
||||
return
|
||||
}
|
||||
|
||||
// Compare key fields
|
||||
if config.Logging != tt.expectedConfig.Logging {
|
||||
t.Errorf("Logging = %v, expected %v", config.Logging, tt.expectedConfig.Logging)
|
||||
}
|
||||
if config.EnableARP != tt.expectedConfig.EnableARP {
|
||||
t.Errorf("EnableARP = %v, expected %v", config.EnableARP, tt.expectedConfig.EnableARP)
|
||||
}
|
||||
if config.EnableBGP != tt.expectedConfig.EnableBGP {
|
||||
t.Errorf("EnableBGP = %v, expected %v", config.EnableBGP, tt.expectedConfig.EnableBGP)
|
||||
}
|
||||
if config.Address != tt.expectedConfig.Address {
|
||||
t.Errorf("Address = %v, expected %v", config.Address, tt.expectedConfig.Address)
|
||||
}
|
||||
if config.Port != tt.expectedConfig.Port {
|
||||
t.Errorf("Port = %v, expected %v", config.Port, tt.expectedConfig.Port)
|
||||
}
|
||||
if config.Interface != tt.expectedConfig.Interface {
|
||||
t.Errorf("Interface = %v, expected %v", config.Interface, tt.expectedConfig.Interface)
|
||||
}
|
||||
|
||||
// Test BGP config if present
|
||||
if tt.expectedConfig.EnableBGP {
|
||||
if config.BGPConfig.RouterID != tt.expectedConfig.BGPConfig.RouterID {
|
||||
t.Errorf("BGPConfig.RouterID = %v, expected %v", config.BGPConfig.RouterID, tt.expectedConfig.BGPConfig.RouterID)
|
||||
}
|
||||
if config.BGPConfig.AS != tt.expectedConfig.BGPConfig.AS {
|
||||
t.Errorf("BGPConfig.AS = %v, expected %v", config.BGPConfig.AS, tt.expectedConfig.BGPConfig.AS)
|
||||
}
|
||||
if len(config.BGPConfig.Peers) != len(tt.expectedConfig.BGPConfig.Peers) {
|
||||
t.Errorf("BGPConfig.Peers length = %v, expected %v", len(config.BGPConfig.Peers), len(tt.expectedConfig.BGPConfig.Peers))
|
||||
}
|
||||
}
|
||||
|
||||
// Test LoadBalancers if present
|
||||
if len(tt.expectedConfig.LoadBalancers) > 0 {
|
||||
if len(config.LoadBalancers) != len(tt.expectedConfig.LoadBalancers) {
|
||||
t.Errorf("LoadBalancers length = %v, expected %v", len(config.LoadBalancers), len(tt.expectedConfig.LoadBalancers))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfigFromFile(t *testing.T) {
|
||||
// Create temporary directory for test files
|
||||
tmpDir, err := os.MkdirTemp("", "kube-vip-merge-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create test config file
|
||||
configFile := filepath.Join(tmpDir, "test-config.yaml")
|
||||
configContent := `
|
||||
logging: 3
|
||||
enableARP: true
|
||||
enableServices: true
|
||||
address: "192.168.1.200"
|
||||
port: 6443
|
||||
interface: "eth1"
|
||||
namespace: "test-namespace"
|
||||
leaseName: "file-lease"
|
||||
leaseDuration: 20
|
||||
prometheusHTTPServer: ":3000"
|
||||
`
|
||||
if err := os.WriteFile(configFile, []byte(configContent), 0600); err != nil {
|
||||
t.Fatalf("Failed to write test config file: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
baseConfig *Config
|
||||
configFilePath string
|
||||
expected *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Merge with empty base config",
|
||||
baseConfig: &Config{},
|
||||
configFilePath: configFile,
|
||||
expected: &Config{
|
||||
Logging: 3,
|
||||
EnableARP: true,
|
||||
EnableServices: true,
|
||||
Address: "192.168.1.200",
|
||||
Port: 6443,
|
||||
Interface: "eth1",
|
||||
Namespace: "test-namespace",
|
||||
PrometheusHTTPServer: ":3000",
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "file-lease",
|
||||
LeaseDuration: 20,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Merge respects existing values (priority test)",
|
||||
baseConfig: &Config{
|
||||
Logging: 5, // Should not be overridden
|
||||
Port: 8443, // Should not be overridden
|
||||
Interface: "eth0", // Should not be overridden
|
||||
},
|
||||
configFilePath: configFile,
|
||||
expected: &Config{
|
||||
Logging: 5, // From base (higher priority)
|
||||
EnableARP: true, // From file
|
||||
EnableServices: true, // From file
|
||||
Address: "192.168.1.200", // From file
|
||||
Port: 8443, // From base (higher priority)
|
||||
Interface: "eth0", // From base (higher priority)
|
||||
Namespace: "test-namespace", // From file
|
||||
PrometheusHTTPServer: ":3000", // From file
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "file-lease", // From file
|
||||
LeaseDuration: 20, // From file
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty config file path",
|
||||
baseConfig: &Config{
|
||||
Logging: 1,
|
||||
},
|
||||
configFilePath: "",
|
||||
expected: &Config{
|
||||
Logging: 1, // Unchanged
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Non-existent config file",
|
||||
baseConfig: &Config{
|
||||
Logging: 1,
|
||||
},
|
||||
configFilePath: "/non/existent/file.yaml",
|
||||
expected: &Config{
|
||||
Logging: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := MergeConfigFromFile(tt.baseConfig, tt.configFilePath)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("MergeConfigFromFile() expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("MergeConfigFromFile() unexpected error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Compare key fields
|
||||
if tt.baseConfig.Logging != tt.expected.Logging {
|
||||
t.Errorf("Logging = %v, expected %v", tt.baseConfig.Logging, tt.expected.Logging)
|
||||
}
|
||||
if tt.baseConfig.EnableARP != tt.expected.EnableARP {
|
||||
t.Errorf("EnableARP = %v, expected %v", tt.baseConfig.EnableARP, tt.expected.EnableARP)
|
||||
}
|
||||
if tt.baseConfig.Address != tt.expected.Address {
|
||||
t.Errorf("Address = %v, expected %v", tt.baseConfig.Address, tt.expected.Address)
|
||||
}
|
||||
if tt.baseConfig.Port != tt.expected.Port {
|
||||
t.Errorf("Port = %v, expected %v", tt.baseConfig.Port, tt.expected.Port)
|
||||
}
|
||||
if tt.baseConfig.Interface != tt.expected.Interface {
|
||||
t.Errorf("Interface = %v, expected %v", tt.baseConfig.Interface, tt.expected.Interface)
|
||||
}
|
||||
if tt.baseConfig.Namespace != tt.expected.Namespace {
|
||||
t.Errorf("Namespace = %v, expected %v", tt.baseConfig.Namespace, tt.expected.Namespace)
|
||||
}
|
||||
if tt.baseConfig.PrometheusHTTPServer != tt.expected.PrometheusHTTPServer {
|
||||
t.Errorf("PrometheusHTTPServer = %v, expected %v", tt.baseConfig.PrometheusHTTPServer, tt.expected.PrometheusHTTPServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfigValues(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseConfig *Config
|
||||
fileConfig *Config
|
||||
expectedBase *Config
|
||||
}{
|
||||
{
|
||||
name: "Merge basic configuration",
|
||||
baseConfig: &Config{
|
||||
Logging: 5, // Should not be overridden
|
||||
Port: 0, // Should be overridden
|
||||
},
|
||||
fileConfig: &Config{
|
||||
Logging: 2,
|
||||
Port: 6443,
|
||||
Interface: "eth0",
|
||||
Address: "192.168.1.100",
|
||||
},
|
||||
expectedBase: &Config{
|
||||
Logging: 5, // From base (non-zero)
|
||||
Port: 6443, // From file (base was zero)
|
||||
Interface: "eth0", // From file (base was empty)
|
||||
Address: "192.168.1.100", // From file (base was empty)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Merge boolean flags",
|
||||
baseConfig: &Config{
|
||||
EnableARP: true, // Should not be overridden
|
||||
},
|
||||
fileConfig: &Config{
|
||||
EnableARP: false, // Should not override true
|
||||
EnableBGP: true, // Should be set
|
||||
EnableServices: true, // Should be set
|
||||
EnableWireguard: false, // Should not be set (false doesn't override false)
|
||||
},
|
||||
expectedBase: &Config{
|
||||
EnableARP: true, // From base (true has priority)
|
||||
EnableBGP: true, // From file
|
||||
EnableServices: true, // From file
|
||||
EnableWireguard: false, // Remains false
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Merge BGP configuration",
|
||||
baseConfig: &Config{
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "1.1.1.1", // Should not be overridden
|
||||
},
|
||||
},
|
||||
fileConfig: &Config{
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "2.2.2.2", // Should not override
|
||||
AS: 65000, // Should be set
|
||||
SourceIF: "eth0", // Should be set
|
||||
HoldTime: 30, // Should be set
|
||||
KeepaliveInterval: 10, // Should be set
|
||||
},
|
||||
},
|
||||
expectedBase: &Config{
|
||||
BGPConfig: BGPConfig{
|
||||
RouterID: "1.1.1.1", // From base (non-empty)
|
||||
AS: 65000, // From file (base was zero)
|
||||
SourceIF: "eth0", // From file (base was empty)
|
||||
HoldTime: 30, // From file (base was zero)
|
||||
KeepaliveInterval: 10, // From file (base was zero)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Merge leader election configuration",
|
||||
baseConfig: &Config{
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "base-lease", // Should not be overridden
|
||||
},
|
||||
},
|
||||
fileConfig: &Config{
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "file-lease", // Should not override
|
||||
LeaseDuration: 15, // Should be set
|
||||
RenewDeadline: 10, // Should be set
|
||||
RetryPeriod: 2, // Should be set
|
||||
},
|
||||
},
|
||||
expectedBase: &Config{
|
||||
KubernetesLeaderElection: KubernetesLeaderElection{
|
||||
LeaseName: "base-lease", // From base (non-empty)
|
||||
LeaseDuration: 15, // From file (base was zero)
|
||||
RenewDeadline: 10, // From file (base was zero)
|
||||
RetryPeriod: 2, // From file (base was zero)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mergeConfigValues(tt.baseConfig, tt.fileConfig)
|
||||
|
||||
// Compare results
|
||||
if tt.baseConfig.Logging != tt.expectedBase.Logging {
|
||||
t.Errorf("Logging = %v, expected %v", tt.baseConfig.Logging, tt.expectedBase.Logging)
|
||||
}
|
||||
if tt.baseConfig.Port != tt.expectedBase.Port {
|
||||
t.Errorf("Port = %v, expected %v", tt.baseConfig.Port, tt.expectedBase.Port)
|
||||
}
|
||||
if tt.baseConfig.Interface != tt.expectedBase.Interface {
|
||||
t.Errorf("Interface = %v, expected %v", tt.baseConfig.Interface, tt.expectedBase.Interface)
|
||||
}
|
||||
if tt.baseConfig.EnableARP != tt.expectedBase.EnableARP {
|
||||
t.Errorf("EnableARP = %v, expected %v", tt.baseConfig.EnableARP, tt.expectedBase.EnableARP)
|
||||
}
|
||||
if tt.baseConfig.EnableBGP != tt.expectedBase.EnableBGP {
|
||||
t.Errorf("EnableBGP = %v, expected %v", tt.baseConfig.EnableBGP, tt.expectedBase.EnableBGP)
|
||||
}
|
||||
if tt.baseConfig.BGPConfig.RouterID != tt.expectedBase.BGPConfig.RouterID {
|
||||
t.Errorf("BGPConfig.RouterID = %v, expected %v", tt.baseConfig.BGPConfig.RouterID, tt.expectedBase.BGPConfig.RouterID)
|
||||
}
|
||||
if tt.baseConfig.BGPConfig.AS != tt.expectedBase.BGPConfig.AS {
|
||||
t.Errorf("BGPConfig.AS = %v, expected %v", tt.baseConfig.BGPConfig.AS, tt.expectedBase.BGPConfig.AS)
|
||||
}
|
||||
if tt.baseConfig.KubernetesLeaderElection.LeaseName != tt.expectedBase.KubernetesLeaderElection.LeaseName {
|
||||
t.Errorf("KubernetesLeaderElection.LeaseName = %v, expected %v", tt.baseConfig.KubernetesLeaderElection.LeaseName, tt.expectedBase.KubernetesLeaderElection.LeaseName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromFile_FileNotExists(t *testing.T) {
|
||||
_, err := LoadConfigFromFile("/non/existent/path/config.yaml")
|
||||
if err == nil {
|
||||
t.Error("LoadConfigFromFile() expected error for non-existent file, got nil")
|
||||
}
|
||||
if !containsString(err.Error(), "config file does not exist") {
|
||||
t.Errorf("LoadConfigFromFile() error = %v, expected to contain 'config file does not exist'", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if a string contains a substring
|
||||
func containsString(str, substr string) bool {
|
||||
return len(str) >= len(substr) && (str == substr || len(substr) == 0 ||
|
||||
(len(substr) > 0 && func() bool {
|
||||
for i := 0; i <= len(str)-len(substr); i++ {
|
||||
if str[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}()))
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
appv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
applyCoreV1 "k8s.io/client-go/applyconfigurations/core/v1"
|
||||
applyMetaV1 "k8s.io/client-go/applyconfigurations/meta/v1"
|
||||
applyRbacV1 "k8s.io/client-go/applyconfigurations/rbac/v1"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
@@ -167,7 +167,11 @@ func GenerateRoleBinding(rolebinding bool, saCfg *applyCoreV1.ServiceAccountAppl
|
||||
}
|
||||
|
||||
// generatePodSpec will take a kube-vip config and generate a Pod spec
|
||||
func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod {
|
||||
func generatePodSpec(c *Config, image, imageVersion string, inCluster bool) *corev1.Pod {
|
||||
imageRef, err := name.NewTag(image, name.WeakValidation, name.WithDefaultTag(imageVersion))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("Cannot parse %q: %w", image, err))
|
||||
}
|
||||
command := "manager"
|
||||
|
||||
// Determine where the pods should be living (for multi-tenancy)
|
||||
@@ -521,10 +525,10 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -592,7 +596,7 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "kube-vip",
|
||||
Image: fmt.Sprintf("ghcr.io/kube-vip/kube-vip:%s", imageVersion),
|
||||
Image: imageRef.Name(),
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
SecurityContext: securityContext,
|
||||
Args: []string{
|
||||
@@ -637,14 +641,14 @@ func generatePodSpec(c *Config, imageVersion string, inCluster bool) *corev1.Pod
|
||||
}
|
||||
|
||||
// GeneratePodManifestFromConfig will take a kube-vip config and generate a manifest
|
||||
func GeneratePodManifestFromConfig(c *Config, imageVersion string, inCluster bool) string {
|
||||
newManifest := generatePodSpec(c, imageVersion, inCluster)
|
||||
func GeneratePodManifestFromConfig(c *Config, image, imageVersion string, inCluster bool) string {
|
||||
newManifest := generatePodSpec(c, image, imageVersion, inCluster)
|
||||
b, _ := yaml.Marshal(newManifest)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// GenerateDaemonsetManifestFromConfig will take a kube-vip config and generate a manifest
|
||||
func GenerateDaemonsetManifestFromConfig(c *Config, imageVersion string, inCluster, taint bool) string {
|
||||
func GenerateDaemonsetManifestFromConfig(c *Config, image, imageVersion string, inCluster, taint bool) string {
|
||||
// Determine where the pod should be deployed
|
||||
var namespace string
|
||||
if c.ServiceNamespace != "" {
|
||||
@@ -653,7 +657,7 @@ func GenerateDaemonsetManifestFromConfig(c *Config, imageVersion string, inClust
|
||||
namespace = metav1.NamespaceSystem
|
||||
}
|
||||
|
||||
podSpec := generatePodSpec(c, imageVersion, inCluster).Spec
|
||||
podSpec := generatePodSpec(c, image, imageVersion, inCluster).Spec
|
||||
newManifest := &appv1.DaemonSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "DaemonSet",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package kubevip
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEnvironment(t *testing.T) {
|
||||
|
||||
@@ -9,8 +12,8 @@ func TestParseEnvironment(t *testing.T) {
|
||||
c *Config
|
||||
wantErr bool
|
||||
}{
|
||||
{"", nil, false},
|
||||
{"", &Config{Interface: "eth0", ServicesInterface: "eth1"}, false},
|
||||
{"nil config", nil, false},
|
||||
{"basic config", &Config{Interface: "eth0", ServicesInterface: "eth1"}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Logf("%v", tt.c)
|
||||
@@ -21,3 +24,53 @@ func TestParseEnvironment(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEnvironmentConfigFile(t *testing.T) {
|
||||
// Save original environment
|
||||
originalConfigFile := os.Getenv("config_file")
|
||||
defer func() {
|
||||
if originalConfigFile != "" {
|
||||
os.Setenv("config_file", originalConfigFile)
|
||||
} else {
|
||||
os.Unsetenv("config_file")
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
envValue string
|
||||
expectedConfig string
|
||||
}{
|
||||
{
|
||||
name: "config_file environment variable set",
|
||||
envValue: "/etc/kube-vip/config.yaml",
|
||||
expectedConfig: "/etc/kube-vip/config.yaml",
|
||||
},
|
||||
{
|
||||
name: "config_file environment variable empty",
|
||||
envValue: "",
|
||||
expectedConfig: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Set environment variable
|
||||
if tt.envValue != "" {
|
||||
os.Setenv("config_file", tt.envValue)
|
||||
} else {
|
||||
os.Unsetenv("config_file")
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
err := ParseEnvironment(config)
|
||||
if err != nil {
|
||||
t.Errorf("ParseEnvironment() unexpected error = %v", err)
|
||||
}
|
||||
|
||||
if config.ConfigFile != tt.expectedConfig {
|
||||
t.Errorf("ConfigFile = %v, expected %v", config.ConfigFile, tt.expectedConfig)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package kubevip
|
||||
|
||||
import (
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
)
|
||||
|
||||
// Config defines all of the settings for the Kube-Vip Pod
|
||||
type Config struct {
|
||||
// Logging, settings
|
||||
@@ -54,6 +50,10 @@ type Config struct {
|
||||
// ArpBroadcastRate, defines how often kube-vip will update the network about updates to the network
|
||||
ArpBroadcastRate int64 `yaml:"arpBroadcastRate"`
|
||||
|
||||
// PreserveVIPOnLeadershipLoss, if true, VIP addresses will remain on interface when leadership is lost (only ARP/NDP broadcasting stops)
|
||||
// If false, VIP addresses are deleted on leadership loss (legacy behavior)
|
||||
PreserveVIPOnLeadershipLoss bool `yaml:"preserveVipOnLeadershipLoss"`
|
||||
|
||||
// Annotations will define if we're going to wait and lookup configuration from Kubernetes node annotations
|
||||
Annotations string
|
||||
|
||||
@@ -128,8 +128,8 @@ type Config struct {
|
||||
CleanRoutingTable bool `yaml:"cleanRoutingTable"`
|
||||
|
||||
// BGP Configuration
|
||||
BGPConfig bgp.Config
|
||||
BGPPeerConfig bgp.Peer
|
||||
BGPConfig BGPConfig
|
||||
BGPPeerConfig BGPPeer
|
||||
BGPPeers []string
|
||||
|
||||
// LoadBalancers are the various services we can load balance over
|
||||
@@ -161,8 +161,8 @@ 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.
|
||||
@@ -180,6 +180,15 @@ type Config struct {
|
||||
|
||||
// HealthCheckPort, if non-zero then will enable the healthcheck to return ok on this port
|
||||
HealthCheckPort int `yaml:"healthCheckPort"`
|
||||
|
||||
// EnableUPNP, enables UPNP functions
|
||||
EnableUPNP bool `yaml:"enableUPNP"`
|
||||
|
||||
// EgressClean, enables egress cleaning on Kube-vip's start
|
||||
EgressClean bool `yaml:"egressClean"`
|
||||
|
||||
// ConfigFile defines the path to a JSON/YAML configuration file
|
||||
ConfigFile string `yaml:"configFile"`
|
||||
}
|
||||
|
||||
// KubernetesLeaderElection defines all of the settings for Kubernetes KubernetesLeaderElection
|
||||
|
||||
5
pkg/kubevip/constants.go
Normal file
5
pkg/kubevip/constants.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package kubevip
|
||||
|
||||
const (
|
||||
LBClassName = "kube-vip.io/kube-vip-class"
|
||||
)
|
||||
7
pkg/kubevip/labels.go
Normal file
7
pkg/kubevip/labels.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package kubevip
|
||||
|
||||
const (
|
||||
// label used on nodes, which announce the LoadBalancer IP
|
||||
HasIP = "kube-vip.io/has-ip"
|
||||
HasIPJSONPath = `kube-vip.io~1has-ip`
|
||||
)
|
||||
127
pkg/lease/lease.go
Normal file
127
pkg/lease/lease.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package lease
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// Manager is used to manage leases.
|
||||
type Manager struct {
|
||||
leases map[string]*Lease
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// NewManager creates new lease manager.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
leases: make(map[string]*Lease),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds lease or incerements counter if lease is alreay used.
|
||||
func (m *Manager) Add(service *v1.Service) (*Lease, bool) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
if _, exist := m.leases[id]; !exist {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.leases[id] = newLease(ctx, cancel)
|
||||
return m.leases[id], true
|
||||
}
|
||||
|
||||
m.leases[id].increment()
|
||||
return m.leases[id], false
|
||||
}
|
||||
|
||||
// Delete decrements lease counter and removes the lease if counter equals 0.
|
||||
func (m *Manager) Delete(service *v1.Service) {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
if _, exist := m.leases[id]; exist {
|
||||
m.leases[id].decrement()
|
||||
if m.leases[id].cnt < 1 {
|
||||
delete(m.leases, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns lease for the service.
|
||||
func (m *Manager) Get(service *v1.Service) *Lease {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
|
||||
if lease, exist := m.leases[id]; exist {
|
||||
return lease
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLeaderContext returns leder context for the service.
|
||||
func (m *Manager) GetLeaderContext(service *v1.Service) context.Context {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
_, id := GetName(service)
|
||||
if _, ok := m.leases[id]; !ok {
|
||||
return nil
|
||||
}
|
||||
return m.leases[id].Ctx
|
||||
}
|
||||
|
||||
// Lease holds lease data.
|
||||
type Lease struct {
|
||||
cnt uint
|
||||
Lock *sync.Mutex
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
Started chan any
|
||||
}
|
||||
|
||||
func newLease(ctx context.Context, cancel context.CancelFunc) *Lease {
|
||||
return &Lease{
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
cnt: 1,
|
||||
Lock: new(sync.Mutex),
|
||||
Started: make(chan any),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lease) increment() {
|
||||
l.Lock.Lock()
|
||||
defer l.Lock.Unlock()
|
||||
l.cnt++
|
||||
}
|
||||
|
||||
func (l *Lease) decrement() {
|
||||
l.Lock.Lock()
|
||||
defer l.Lock.Unlock()
|
||||
if l.cnt == 0 {
|
||||
return
|
||||
}
|
||||
l.cnt--
|
||||
if l.cnt < 1 {
|
||||
l.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// GetName gets lease name and id for the service.
|
||||
func GetName(service *v1.Service) (string, string) {
|
||||
serviceLease, exists := service.Annotations[kubevip.ServiceLease]
|
||||
if !exists || serviceLease == "" {
|
||||
serviceLease = fmt.Sprintf("kubevip-%s", service.Name)
|
||||
}
|
||||
serviceLeaseID := fmt.Sprintf("%s/%s", serviceLease, service.Namespace)
|
||||
return serviceLease, serviceLeaseID
|
||||
}
|
||||
|
||||
// UsesCommon checks if service uses common lease feature.
|
||||
func UsesCommon(service *v1.Service) bool {
|
||||
_, common := service.Annotations[kubevip.ServiceLease]
|
||||
return common
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/cloudflare/ipvs/netmask"
|
||||
"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/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
@@ -358,7 +358,7 @@ func (lb *IPVSLoadBalancer) isLocal(address string) (bool, error) {
|
||||
}
|
||||
|
||||
family := netlink.FAMILY_V6
|
||||
if vip.IsIPv4(address) {
|
||||
if utils.IsIPv4(address) {
|
||||
family = netlink.FAMILY_V4
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/cloudflare/ipvs"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
)
|
||||
|
||||
func Test_ipAndFamily(t *testing.T) {
|
||||
@@ -19,7 +20,7 @@ func Test_ipAndFamily(t *testing.T) {
|
||||
want1 ipvs.AddressFamily
|
||||
}{
|
||||
{
|
||||
name: "IPv4",
|
||||
name: utils.IPv4Family,
|
||||
args: args{
|
||||
address: "192.168.0.20",
|
||||
},
|
||||
@@ -27,7 +28,7 @@ func Test_ipAndFamily(t *testing.T) {
|
||||
want1: ipvs.INET,
|
||||
},
|
||||
{
|
||||
name: "IPv6",
|
||||
name: utils.IPv6Family,
|
||||
args: args{
|
||||
address: "ff02::3",
|
||||
},
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package manager
|
||||
|
||||
const (
|
||||
|
||||
// The host that has the VIP
|
||||
vipHost = "kube-vip.io/vipHost"
|
||||
|
||||
// Enable egress on a service
|
||||
egress = "kube-vip.io/egress"
|
||||
|
||||
// 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"
|
||||
|
||||
upnpEnabled = "kube-vip.io/forwardUPNP"
|
||||
)
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -17,15 +16,14 @@ import (
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"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/trafficmirror"
|
||||
"github.com/kube-vip/kube-vip/pkg/node"
|
||||
"github.com/kube-vip/kube-vip/pkg/services"
|
||||
"github.com/kube-vip/kube-vip/pkg/upnp"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
@@ -42,11 +40,6 @@ type Manager struct {
|
||||
// Manager services
|
||||
// service bool
|
||||
|
||||
// Keeps track of all running instances
|
||||
serviceInstances []*cluster.Instance
|
||||
|
||||
// UPNP functionality
|
||||
upnp bool
|
||||
// BGP Manager, this is a singleton that manages all BGP advertisements
|
||||
bgpServer *bgp.Server
|
||||
|
||||
@@ -56,6 +49,8 @@ type Manager struct {
|
||||
// This channel is used to signal a shutdown
|
||||
shutdownChan chan struct{}
|
||||
|
||||
svcProcessor *services.Processor
|
||||
|
||||
// This is a prometheus counter used to count the number of events received
|
||||
// from the service watcher
|
||||
countServiceWatchEvent *prometheus.CounterVec
|
||||
@@ -72,6 +67,11 @@ type Manager struct {
|
||||
|
||||
// This tracks VIPs and performs ARP/NDP advertisement.
|
||||
arpMgr *arp.Manager
|
||||
|
||||
// This tracks node labels and performs label management
|
||||
// implementation will be decided in constructor
|
||||
// based on config.EnableNodeLabeling
|
||||
nodeLabelManager node.LabelManager
|
||||
}
|
||||
|
||||
// New will create a new managing object
|
||||
@@ -177,6 +177,36 @@ func New(configMap string, config *kubevip.Config) (*Manager, error) {
|
||||
// }
|
||||
// }
|
||||
|
||||
// listen for interrupts or the Linux SIGTERM signal and cancel
|
||||
// our context, which the leader election code will observe and
|
||||
// step down
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
// Add Notification for Userland interrupt
|
||||
signal.Notify(signalChan, syscall.SIGINT)
|
||||
|
||||
// Add Notification for SIGTERM (sent from Kubernetes)
|
||||
signal.Notify(signalChan, syscall.SIGTERM)
|
||||
|
||||
// All watchers and other goroutines should have an additional goroutine that blocks on this, to shut things down
|
||||
shutdownChan := make(chan struct{})
|
||||
|
||||
intfMgr := networkinterface.NewManager()
|
||||
arpMgr := arp.NewManager(config)
|
||||
|
||||
// create the node label manager
|
||||
// constructor will decide if it should be a noop or not
|
||||
nodeLabelManager := node.NewManager(config, clientset)
|
||||
|
||||
var bgpServer *bgp.Server
|
||||
if config.EnableBGP {
|
||||
bgpServer, err = bgp.NewBGPServer(config.BGPConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating BGP server: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
svcProcessor := services.NewServicesProcessor(config, bgpServer, clientset, rwClientSet, shutdownChan, intfMgr, arpMgr, nodeLabelManager)
|
||||
|
||||
return &Manager{
|
||||
clientSet: clientset,
|
||||
rwClientSet: rwClientSet,
|
||||
@@ -194,8 +224,13 @@ 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"}),
|
||||
intfMgr: networkinterface.NewManager(),
|
||||
arpMgr: arp.NewManager(config),
|
||||
signalChan: signalChan,
|
||||
shutdownChan: shutdownChan,
|
||||
svcProcessor: svcProcessor,
|
||||
intfMgr: intfMgr,
|
||||
arpMgr: arpMgr,
|
||||
bgpServer: bgpServer,
|
||||
nodeLabelManager: nodeLabelManager,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -211,6 +246,9 @@ func (sm *Manager) Start() error {
|
||||
// Add Notification for SIGTERM (sent from Kubernetes)
|
||||
signal.Notify(sm.signalChan, syscall.SIGTERM)
|
||||
|
||||
// Add Notification for SIGUSR1 (for configuration dump)
|
||||
signal.Notify(sm.signalChan, syscall.SIGUSR1)
|
||||
|
||||
// All watchers and other goroutines should have an additional goroutine that blocks on this, to shut things down
|
||||
sm.shutdownChan = make(chan struct{})
|
||||
|
||||
@@ -234,6 +272,13 @@ func (sm *Manager) Start() error {
|
||||
}()
|
||||
}
|
||||
|
||||
// on exit, clean up the node labels
|
||||
defer func() {
|
||||
if err := sm.nodeLabelManager.CleanUpLabels(10 * time.Second); err != nil {
|
||||
log.Error("CleanUpNodeLabels", "unable to cleanup node labels", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// If BGP is enabled then we start a server instance that will broadcast VIPs
|
||||
if sm.config.EnableBGP {
|
||||
|
||||
@@ -248,16 +293,12 @@ func (sm *Manager) Start() error {
|
||||
}
|
||||
|
||||
if sm.config.EnableARP || sm.config.EnableWireguard {
|
||||
// Before starting the leader Election enable any additional functionality
|
||||
upnpEnabled, _ := strconv.ParseBool(os.Getenv("enableUPNP"))
|
||||
|
||||
if upnpEnabled {
|
||||
sm.upnp = true
|
||||
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.upnp = false
|
||||
sm.config.EnableUPNP = false
|
||||
} else {
|
||||
for _, c := range clients {
|
||||
ip, err := c.GetExternalIPAddress()
|
||||
@@ -265,12 +306,11 @@ func (sm *Manager) Start() error {
|
||||
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()
|
||||
}
|
||||
// TODO: It would be nice to run the UPNP refresh only on the leader.
|
||||
go sm.refreshUPNPForwards()
|
||||
}
|
||||
|
||||
// If ARP is enabled then we start a LeaderElection that will use ARP to advertise VIPs
|
||||
@@ -315,76 +355,3 @@ func (sm *Manager) parseAnnotations() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) serviceInterface() string {
|
||||
svcIf := sm.config.Interface
|
||||
if sm.config.ServicesInterface != "" {
|
||||
svcIf = sm.config.ServicesInterface
|
||||
}
|
||||
return svcIf
|
||||
}
|
||||
|
||||
func (sm *Manager) startTrafficMirroringIfEnabled() error {
|
||||
if sm.config.MirrorDestInterface != "" {
|
||||
svcIf := sm.serviceInterface()
|
||||
log.Info("mirroring traffic", "src", svcIf, "dest", sm.config.MirrorDestInterface)
|
||||
if err := trafficmirror.MirrorTrafficFromNIC(svcIf, sm.config.MirrorDestInterface); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("skip starting traffic mirroring since it's not enabled.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) stopTrafficMirroringIfEnabled() error {
|
||||
if sm.config.MirrorDestInterface != "" {
|
||||
svcIf := sm.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
|
||||
}
|
||||
|
||||
func (sm *Manager) findServiceInstance(svc *v1.Service) *cluster.Instance {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
if svc == nil {
|
||||
return nil // If the service is nil then we cannot find it
|
||||
}
|
||||
log.Debug("finding service", "UID", svc.UID, "name", svc.Name, "namespace", svc.Namespace)
|
||||
for i := range sm.serviceInstances {
|
||||
log.Debug("saved service", "UID", svc.UID, "name", svc.Name, "namespace", svc.Namespace)
|
||||
if sm.serviceInstances[i].ServiceSnapshot.UID == svc.UID {
|
||||
log.Debug("found service instance", "UID", svc.UID, "name", svc.Name, "namespace", svc.Namespace)
|
||||
return sm.serviceInstances[i]
|
||||
}
|
||||
}
|
||||
log.Debug("service instance not found", "UID", svc.UID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh UPNP Port Forwards for all Service Instances registered in the SM
|
||||
func (sm *Manager) refreshUPNPForwards() {
|
||||
log.Info("Starting UPNP Port Refresher")
|
||||
for {
|
||||
time.Sleep(300 * time.Second)
|
||||
|
||||
log.Info("[UPNP] Refreshing Instances", "number of instances", len(sm.serviceInstances))
|
||||
func() {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
for i := range sm.serviceInstances {
|
||||
sm.upnpMap(context.TODO(), sm.serviceInstances[i])
|
||||
if err := sm.updateStatus(sm.serviceInstances[i]); err != nil {
|
||||
log.Warn("[UPNP] Error updating service", "ip", sm.serviceInstances[i].ServiceSnapshot.Name, "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -33,15 +32,24 @@ func (sm *Manager) startARP(id string) error {
|
||||
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-sm.signalChan
|
||||
log.Info("Received kube-vip termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster.Stop()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received kube-vip termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster.Stop()
|
||||
}
|
||||
// Close all go routines
|
||||
close(sm.shutdownChan)
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Close all go routines
|
||||
close(sm.shutdownChan)
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if sm.config.EnableControlPlane {
|
||||
@@ -83,7 +91,7 @@ func (sm *Manager) startARP(id string) error {
|
||||
}
|
||||
|
||||
// This will tidy any dangling kube-vip iptables rules
|
||||
if os.Getenv("EGRESS_CLEAN") != "" {
|
||||
if sm.config.EgressClean {
|
||||
vip.ClearIPTables(sm.config.EgressWithNftables, sm.config.ServiceNamespace, iptables.ProtocolIPv4)
|
||||
}
|
||||
|
||||
@@ -91,7 +99,7 @@ func (sm *Manager) startARP(id string) error {
|
||||
// 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.startServicesWatchForLeaderElection(ctx)
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -126,7 +134,7 @@ func (sm *Manager) startARP(id string) 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("service watcher", "err", err)
|
||||
panic("") // TODO: - emulating log.fatal here
|
||||
@@ -137,11 +145,7 @@ func (sm *Manager) startARP(id string) error {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
log.Info("leader lost", "new leader", id)
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
sm.svcProcessor.Stop()
|
||||
|
||||
log.Error("lost leadership, restarting kube-vip")
|
||||
panic("") // TODO: - emulating log.fatal here
|
||||
|
||||
@@ -19,8 +19,15 @@ func (sm *Manager) startBGP() error {
|
||||
// var ns string
|
||||
var err error
|
||||
|
||||
if sm.bgpServer == nil {
|
||||
sm.bgpServer, err = bgp.NewBGPServer(sm.config.BGPConfig)
|
||||
if err != nil {
|
||||
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)
|
||||
@@ -36,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
|
||||
@@ -55,15 +61,24 @@ func (sm *Manager) startBGP() error {
|
||||
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-sm.signalChan
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
if cpCluster != nil {
|
||||
cpCluster.Stop()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
if cpCluster != nil {
|
||||
cpCluster.Stop()
|
||||
}
|
||||
}
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if sm.config.EnableControlPlane {
|
||||
@@ -99,9 +114,18 @@ func (sm *Manager) startBGP() error {
|
||||
}
|
||||
}
|
||||
|
||||
err = sm.servicesWatcher(ctx, sm.syncServices)
|
||||
if err != nil {
|
||||
return err
|
||||
if sm.config.EnableServicesElection {
|
||||
log.Info("beginning watching services, leaderelection will happen for every service")
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Info("beginning watching services without leader election")
|
||||
err = sm.svcProcessor.ServicesWatcher(ctx, sm.svcProcessor.SyncServices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Shutting down Kube-Vip")
|
||||
|
||||
235
pkg/manager/manager_dump.go
Normal file
235
pkg/manager/manager_dump.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/nftables"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// dumpConfiguration prints the current configuration to stdout when SIGUSR1 is received
|
||||
func (sm *Manager) dumpConfiguration() {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf(" KUBE-VIP CONFIGURATION DUMP\n")
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf("Timestamp: %s\n", time.Now().Format(time.RFC3339))
|
||||
fmt.Printf("Node Name: %s\n", sm.config.NodeName)
|
||||
fmt.Printf("Process ID: %d\n", os.Getpid())
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf("\n")
|
||||
|
||||
sm.dumpConfigSection()
|
||||
sm.dumpBGPSection()
|
||||
sm.dumpARPSection()
|
||||
sm.dumpServicesSection()
|
||||
sm.dumpNetworkInterfacesSection()
|
||||
sm.dumpLeaderElectionSection()
|
||||
sm.dumpRuntimeSection()
|
||||
sm.dumpNFTablesSection()
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf(" END OF CONFIGURATION DUMP\n")
|
||||
fmt.Printf("================================================================================\n")
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpConfigSection() {
|
||||
fmt.Printf("--- BASIC CONFIGURATION ---\n")
|
||||
fmt.Printf("VIP: %s\n", sm.config.Address)
|
||||
fmt.Printf("VIP Subnet: %s\n", sm.config.VIPSubnet)
|
||||
fmt.Printf("Port: %d\n", sm.config.Port)
|
||||
fmt.Printf("Namespace: %s\n", sm.config.Namespace)
|
||||
fmt.Printf("Service Namespace: %s\n", sm.config.ServiceNamespace)
|
||||
fmt.Printf("Interface: %s\n", sm.config.Interface)
|
||||
fmt.Printf("Services Interface: %s\n", sm.config.ServicesInterface)
|
||||
fmt.Printf("Single Node Mode: %t\n", sm.config.SingleNode)
|
||||
fmt.Printf("Start As Leader: %t\n", sm.config.StartAsLeader)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpBGPSection() {
|
||||
fmt.Printf("--- BGP CONFIGURATION ---\n")
|
||||
fmt.Printf("BGP Enabled: %t\n", sm.config.EnableBGP)
|
||||
if sm.config.EnableBGP {
|
||||
fmt.Printf("BGP AS: %d\n", sm.config.BGPConfig.AS)
|
||||
fmt.Printf("BGP Router ID: %s\n", sm.config.BGPConfig.RouterID)
|
||||
fmt.Printf("BGP Source IP: %s\n", sm.config.BGPConfig.SourceIP)
|
||||
fmt.Printf("BGP Source Interface: %s\n", sm.config.BGPConfig.SourceIF)
|
||||
fmt.Printf("BGP Hold Time: %d\n", sm.config.BGPConfig.HoldTime)
|
||||
fmt.Printf("BGP Keepalive Interval: %d\n", sm.config.BGPConfig.KeepaliveInterval)
|
||||
fmt.Printf("BGP Peers: %d\n", len(sm.config.BGPConfig.Peers))
|
||||
for i, peer := range sm.config.BGPConfig.Peers {
|
||||
fmt.Printf(" Peer %d: %s:%d (AS: %d, MultiHop: %t)\n",
|
||||
i+1, peer.Address, peer.Port, peer.AS, peer.MultiHop)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpARPSection() {
|
||||
fmt.Printf("--- ARP/NDP CONFIGURATION ---\n")
|
||||
fmt.Printf("ARP Enabled: %t\n", sm.config.EnableARP)
|
||||
if sm.config.EnableARP {
|
||||
fmt.Printf("ARP Broadcast Rate: %d\n", sm.config.ArpBroadcastRate)
|
||||
}
|
||||
fmt.Printf("Wireguard Enabled: %t\n", sm.config.EnableWireguard)
|
||||
fmt.Printf("Routing Table Enabled: %t\n", sm.config.EnableRoutingTable)
|
||||
if sm.config.EnableRoutingTable {
|
||||
fmt.Printf("Routing Table ID: %d\n", sm.config.RoutingTableID)
|
||||
fmt.Printf("Routing Protocol: %d\n", sm.config.RoutingProtocol)
|
||||
fmt.Printf("Clean Routing Table: %t\n", sm.config.CleanRoutingTable)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpServicesSection() {
|
||||
fmt.Printf("--- SERVICES CONFIGURATION ---\n")
|
||||
fmt.Printf("Services Enabled: %t\n", sm.config.EnableServices)
|
||||
if sm.config.EnableServices {
|
||||
fmt.Printf("Services Election: %t\n", sm.config.EnableServicesElection)
|
||||
fmt.Printf("Load Balancer Class Only: %t\n", sm.config.LoadBalancerClassOnly)
|
||||
fmt.Printf("Load Balancer Class Name: %s\n", sm.config.LoadBalancerClassName)
|
||||
fmt.Printf("Disable Service Updates: %t\n", sm.config.DisableServiceUpdates)
|
||||
fmt.Printf("Enable Endpoints: %t\n", sm.config.EnableEndpoints)
|
||||
fmt.Printf("Service Security Enabled: %t\n", sm.config.EnableServiceSecurity)
|
||||
|
||||
if sm.svcProcessor != nil {
|
||||
instances := sm.svcProcessor.ServiceInstances
|
||||
fmt.Printf("Kube-vip Active Service Instances: %d\n", len(instances))
|
||||
for i, inst := range instances {
|
||||
if inst.ServiceSnapshot != nil {
|
||||
svc := inst.ServiceSnapshot
|
||||
vipConfigs := ""
|
||||
for j, cfg := range svc.Status.LoadBalancer.Ingress {
|
||||
if j > 0 {
|
||||
vipConfigs += ", "
|
||||
}
|
||||
vipConfigs += cfg.IP
|
||||
}
|
||||
fmt.Printf(" Service %d: %s/%s (Type: %s, VIPs: %s)\n",
|
||||
i+1, svc.Namespace, svc.Name, svc.Spec.Type, vipConfigs)
|
||||
}
|
||||
}
|
||||
}
|
||||
if sm.clientSet != nil {
|
||||
fmt.Println()
|
||||
// Kubernetes configuration
|
||||
fmt.Println("--- KUBERNETES CONFIGURATION (SERVICES/ENDPOINTSLICES) ---")
|
||||
|
||||
fmt.Println("Service Configuration:")
|
||||
svcList, err := sm.clientSet.CoreV1().Services(v1.NamespaceAll).List(context.TODO(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
fmt.Println("Unable to retrieve all Services")
|
||||
} else {
|
||||
for x := range svcList.Items {
|
||||
|
||||
// Build all addresses
|
||||
vipConfigs := ""
|
||||
for j, cfg := range svcList.Items[x].Status.LoadBalancer.Ingress {
|
||||
if j > 0 {
|
||||
vipConfigs += ", "
|
||||
}
|
||||
vipConfigs += cfg.IP
|
||||
}
|
||||
fmt.Printf("Name=%s, UUID=%s, Addresses=%s\n", svcList.Items[x].Name, string(svcList.Items[x].UID), vipConfigs)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("EndpointSlice Configuration (note endpoint names have -XXXXX prefixed):")
|
||||
epList, err := sm.clientSet.DiscoveryV1().EndpointSlices(v1.NamespaceAll).List(context.TODO(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
fmt.Println("Unable to retrieve all EndpointSlices")
|
||||
} else {
|
||||
for x := range epList.Items {
|
||||
// Build all addresses
|
||||
fmt.Printf(" Endpoint Slice Name: %s\n", epList.Items[x].Name)
|
||||
for _, ep := range epList.Items[x].Endpoints {
|
||||
endpoints := ""
|
||||
for i, addresses := range ep.Addresses {
|
||||
if i > 0 {
|
||||
endpoints += ", "
|
||||
}
|
||||
endpoints += addresses
|
||||
}
|
||||
nodeName := "Unknown"
|
||||
targetPod := "Unknown"
|
||||
if ep.NodeName != nil {
|
||||
nodeName = *ep.NodeName
|
||||
}
|
||||
if ep.TargetRef != nil {
|
||||
targetPod = ep.TargetRef.Name
|
||||
}
|
||||
fmt.Printf("\tNode: %s, Target Pod:%s, Addresses: %s\n", nodeName, targetPod, endpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpNetworkInterfacesSection() {
|
||||
fmt.Printf("--- NETWORK INTERFACES ---\n")
|
||||
fmt.Printf("Network Interface Manager: %t\n", sm.intfMgr != nil)
|
||||
fmt.Printf("ARP Manager: %t\n", sm.arpMgr != nil)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpLeaderElectionSection() {
|
||||
fmt.Printf("--- LEADER ELECTION CONFIGURATION ---\n")
|
||||
fmt.Printf("Control Plane Enabled: %t\n", sm.config.EnableControlPlane)
|
||||
if sm.config.EnableControlPlane {
|
||||
fmt.Printf("Detect Control Plane: %t\n", sm.config.DetectControlPlane)
|
||||
}
|
||||
fmt.Printf("Leader Election Type: %s\n", sm.config.LeaderElectionType)
|
||||
fmt.Printf("Leader Election Enabled: %t\n", sm.config.EnableLeaderElection)
|
||||
if sm.config.EnableLeaderElection {
|
||||
fmt.Printf("Lease Name: %s\n", sm.config.LeaseName)
|
||||
fmt.Printf("Lease Duration: %d seconds\n", sm.config.LeaseDuration)
|
||||
fmt.Printf("Renew Deadline: %d seconds\n", sm.config.RenewDeadline)
|
||||
fmt.Printf("Retry Period: %d seconds\n", sm.config.RetryPeriod)
|
||||
}
|
||||
fmt.Printf("Services Lease Name: %s\n", sm.config.ServicesLeaseName)
|
||||
fmt.Printf("Node Labeling Enabled: %t\n", sm.config.EnableNodeLabeling)
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpRuntimeSection() {
|
||||
fmt.Printf("--- RUNTIME STATISTICS ---\n")
|
||||
fmt.Printf("Load Balancer Enabled: %t\n", sm.config.EnableLoadBalancer)
|
||||
if sm.config.EnableLoadBalancer {
|
||||
fmt.Printf("Load Balancer Port: %d\n", sm.config.LoadBalancerPort)
|
||||
fmt.Printf("Load Balancer Forwarding Method: %s\n", sm.config.LoadBalancerForwardingMethod)
|
||||
fmt.Printf("Load Balancers Configured: %d\n", len(sm.config.LoadBalancers))
|
||||
}
|
||||
fmt.Printf("Prometheus HTTP Server: %s\n", sm.config.PrometheusHTTPServer)
|
||||
fmt.Printf("Health Check Port: %d\n", sm.config.HealthCheckPort)
|
||||
fmt.Printf("UPNP Enabled: %t\n", sm.config.EnableUPNP)
|
||||
fmt.Printf("Egress Clean Enabled: %t\n", sm.config.EgressClean)
|
||||
if sm.config.EgressClean {
|
||||
fmt.Printf("Egress with nftables: %t\n", sm.config.EgressWithNftables)
|
||||
fmt.Printf("Egress Pod CIDR: %s\n", sm.config.EgressPodCidr)
|
||||
fmt.Printf("Egress Service CIDR: %s\n", sm.config.EgressServiceCidr)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (sm *Manager) dumpNFTablesSection() {
|
||||
fmt.Printf("--- NFTABLES CONFIGURATION ---\n")
|
||||
chains, err := nftables.ListChains()
|
||||
if err != nil {
|
||||
fmt.Printf("Unable to retrieve NFTables chains, error=%s", err)
|
||||
}
|
||||
for x := range chains {
|
||||
fmt.Printf("Chain: %s\n", chains[x])
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
238
pkg/manager/manager_dump_test.go
Normal file
238
pkg/manager/manager_dump_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDumpConfiguration(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
Address: "192.168.1.100",
|
||||
Interface: "eth0",
|
||||
Port: 6443,
|
||||
EnableARP: true,
|
||||
EnableBGP: false,
|
||||
EnableControlPlane: true,
|
||||
EnableServices: true,
|
||||
LeaderElectionType: "kubernetes",
|
||||
Namespace: "kube-system",
|
||||
NodeName: "test-node",
|
||||
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
|
||||
EnableLeaderElection: true,
|
||||
LeaseName: "test-lease",
|
||||
},
|
||||
}
|
||||
|
||||
mgr := &Manager{
|
||||
config: config,
|
||||
}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpConfiguration()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "KUBE-VIP CONFIGURATION DUMP", "should contain header")
|
||||
assert.Contains(t, output, "Node Name: test-node", "should contain node name")
|
||||
assert.Contains(t, output, "VIP: 192.168.1.100", "should contain VIP address")
|
||||
assert.Contains(t, output, "Interface: eth0", "should contain interface")
|
||||
assert.Contains(t, output, "Port: 6443", "should contain port")
|
||||
assert.Contains(t, output, "ARP Enabled: true", "should contain ARP status")
|
||||
assert.Contains(t, output, "BGP Enabled: false", "should contain BGP status")
|
||||
}
|
||||
|
||||
func TestDumpConfigSection(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
Address: "192.168.1.100",
|
||||
Interface: "eth0",
|
||||
Port: 6443,
|
||||
VIPSubnet: "/24",
|
||||
EnableARP: true,
|
||||
EnableBGP: false,
|
||||
EnableControlPlane: true,
|
||||
EnableServices: true,
|
||||
LeaderElectionType: "kubernetes",
|
||||
Namespace: "kube-system",
|
||||
KubernetesLeaderElection: kubevip.KubernetesLeaderElection{
|
||||
EnableLeaderElection: true,
|
||||
LeaseName: "test-lease",
|
||||
},
|
||||
}
|
||||
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpConfigSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "--- BASIC CONFIGURATION ---")
|
||||
assert.Contains(t, output, "VIP: 192.168.1.100")
|
||||
assert.Contains(t, output, "VIP Subnet: /24")
|
||||
assert.Contains(t, output, "Interface: eth0")
|
||||
assert.Contains(t, output, "Port: 6443")
|
||||
assert.Contains(t, output, "Namespace: kube-system")
|
||||
assert.Contains(t, output, "Single Node Mode: false")
|
||||
assert.Contains(t, output, "Start As Leader: false")
|
||||
}
|
||||
|
||||
func TestDumpBGPSection(t *testing.T) {
|
||||
t.Run("BGP disabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableBGP: false,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpBGPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "BGP Enabled: false")
|
||||
})
|
||||
|
||||
t.Run("BGP enabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableBGP: true,
|
||||
BGPConfig: kubevip.BGPConfig{
|
||||
RouterID: "192.168.1.1",
|
||||
AS: 65000,
|
||||
Peers: []kubevip.BGPPeer{
|
||||
{Address: "192.168.1.2", AS: 65001},
|
||||
{Address: "192.168.1.3", AS: 65002},
|
||||
},
|
||||
},
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpBGPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "BGP Enabled: true")
|
||||
assert.Contains(t, output, "BGP Router ID: 192.168.1.1")
|
||||
assert.Contains(t, output, "BGP AS: 65000")
|
||||
assert.Contains(t, output, "BGP Peers: 2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDumpARPSection(t *testing.T) {
|
||||
t.Run("ARP disabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableARP: false,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpARPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "ARP Enabled: false")
|
||||
})
|
||||
|
||||
t.Run("ARP enabled", func(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableARP: true,
|
||||
ArpBroadcastRate: 5,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpARPSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "ARP Enabled: true")
|
||||
assert.Contains(t, output, "ARP Broadcast Rate: 5")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDumpRuntimeSection(t *testing.T) {
|
||||
config := &kubevip.Config{
|
||||
EnableLoadBalancer: false,
|
||||
PrometheusHTTPServer: "",
|
||||
HealthCheckPort: 0,
|
||||
EnableUPNP: false,
|
||||
EgressClean: false,
|
||||
}
|
||||
mgr := &Manager{config: config}
|
||||
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
mgr.dumpRuntimeSection()
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err := io.Copy(&buf, r)
|
||||
assert.NoError(t, err, "io.Copy should not return error")
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "--- RUNTIME STATISTICS ---", "should contain runtime section header")
|
||||
assert.Contains(t, output, "Load Balancer Enabled: false", "should contain load balancer status")
|
||||
assert.Contains(t, output, "UPNP Enabled: false", "should contain UPNP status")
|
||||
}
|
||||
@@ -3,14 +3,13 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"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"
|
||||
"github.com/vishvananda/netlink"
|
||||
@@ -40,48 +39,34 @@ func (sm *Manager) startTableMode(id string) error {
|
||||
}()
|
||||
}
|
||||
|
||||
egressCleanEnv := os.Getenv("EGRESS_CLEAN")
|
||||
if egressCleanEnv != "" {
|
||||
egressClean, err := strconv.ParseBool(egressCleanEnv)
|
||||
if err != nil {
|
||||
log.Warn("failed to parse EGRESS_CLEAN env value [%s]. Egress cleaning will not be performed: %s", egressCleanEnv, err.Error())
|
||||
}
|
||||
if 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")
|
||||
}
|
||||
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 kube-vip termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster.Stop()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received kube-vip termination, signaling shutdown")
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster.Stop()
|
||||
}
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
}()
|
||||
|
||||
if sm.config.EnableControlPlane {
|
||||
cpCluster, err = cluster.InitCluster(sm.config, false, sm.intfMgr, sm.arpMgr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cluster initialization error: %w", err)
|
||||
}
|
||||
|
||||
clusterManager, err := initClusterManager(sm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cluster manager initialization error: %w", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
if sm.config.EnableServices {
|
||||
log.Debug("starting Services")
|
||||
ns, err := returnNameSpace()
|
||||
if err != nil {
|
||||
log.Warn("unable to auto-detect namespace", "dropping to", sm.config.Namespace)
|
||||
@@ -92,7 +77,7 @@ func (sm *Manager) startTableMode(id string) error {
|
||||
// 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.startServicesWatchForLeaderElection(ctx)
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -126,7 +111,7 @@ func (sm *Manager) startTableMode(id string) 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.Error())
|
||||
panic("")
|
||||
@@ -137,11 +122,7 @@ func (sm *Manager) startTableMode(id string) error {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
log.Info("leader lost", "id", id)
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
sm.svcProcessor.Stop()
|
||||
|
||||
log.Error("lost leadership, restarting kube-vip")
|
||||
panic("")
|
||||
@@ -158,12 +139,37 @@ func (sm *Manager) startTableMode(id string) error {
|
||||
})
|
||||
} else {
|
||||
log.Info("beginning watching services without leader election")
|
||||
err = sm.servicesWatcher(ctx, sm.syncServices)
|
||||
err = sm.svcProcessor.ServicesWatcher(ctx, sm.svcProcessor.SyncServices)
|
||||
if err != nil {
|
||||
log.Error("Cannot watch services", "err", err)
|
||||
} else {
|
||||
log.Debug("watching services")
|
||||
}
|
||||
}
|
||||
}
|
||||
if sm.config.EnableControlPlane {
|
||||
log.Debug("initCluster for ControlPlane")
|
||||
cpCluster, err = cluster.InitCluster(sm.config, false, sm.intfMgr, sm.arpMgr)
|
||||
if err != nil {
|
||||
log.Debug("init of ControlPlane NOT successful")
|
||||
return fmt.Errorf("cluster initialization error: %w", err)
|
||||
}
|
||||
log.Debug("init of ControlPlane successful")
|
||||
log.Debug("init ClusterManager")
|
||||
clusterManager, err := initClusterManager(sm)
|
||||
if err != nil {
|
||||
log.Debug("init cluster manager NOT successful")
|
||||
return fmt.Errorf("cluster manager initialization error: %w", err)
|
||||
}
|
||||
log.Debug("init ClusterManager successful")
|
||||
if err := cpCluster.StartVipService(sm.config, clusterManager, nil); err != nil {
|
||||
log.Error("Control Plane", "err", err)
|
||||
// Trigger the shutdown of this manager instance
|
||||
sm.signalChan <- syscall.SIGINT
|
||||
} else {
|
||||
log.Debug("start VipServer for cluster manager successful")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -181,21 +187,17 @@ func (sm *Manager) cleanRoutes() error {
|
||||
if sm.config.EnableControlPlane {
|
||||
found = (routes[i].Dst.IP.String() == sm.config.Address)
|
||||
} else {
|
||||
found = sm.countRouteReferences(&routes[i]) > 0
|
||||
found = endpoints.CountRouteReferences(&routes[i], &sm.svcProcessor.ServiceInstances) > 0
|
||||
}
|
||||
|
||||
if !found {
|
||||
err = netlink.RouteDel(&(routes[i]))
|
||||
if err != nil {
|
||||
log.Error("[route] failed to delete", "route", routes[i], "err", err)
|
||||
} else {
|
||||
log.Debug("[route] deleted", "route", routes[i])
|
||||
log.Error("[route] deletion", "route", routes[i], "err", err)
|
||||
}
|
||||
log.Debug("[route] deletion", "route", routes[i])
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) countRouteReferences(route *netlink.Route) int {
|
||||
return cluster.CountRouteReferences(&sm.serviceInstances, route)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
@@ -39,11 +40,19 @@ func (sm *Manager) startWireguard(id string) error {
|
||||
|
||||
// Shutdown function that will wait on this signal, unless we call it ourselves
|
||||
go func() {
|
||||
<-sm.signalChan
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
for {
|
||||
sig := <-sm.signalChan
|
||||
switch sig {
|
||||
case syscall.SIGUSR1:
|
||||
log.Info("Received SIGUSR1, dumping configuration")
|
||||
sm.dumpConfiguration()
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
log.Info("Received termination, signaling shutdown")
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ns, err = returnNameSpace()
|
||||
@@ -56,7 +65,7 @@ func (sm *Manager) startWireguard(id string) error {
|
||||
// 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.startServicesWatchForLeaderElection(ctx)
|
||||
err = sm.svcProcessor.StartServicesWatchForLeaderElection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,7 +100,7 @@ func (sm *Manager) startWireguard(id string) 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.Error())
|
||||
panic("")
|
||||
@@ -102,11 +111,7 @@ func (sm *Manager) startWireguard(id string) error {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
log.Info("leader lost", "id", id)
|
||||
for _, instance := range sm.serviceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
sm.svcProcessor.Stop()
|
||||
|
||||
log.Error("lost leadership, restarting kube-vip")
|
||||
panic("")
|
||||
|
||||
@@ -7,16 +7,12 @@ import (
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
nodeLabelIndex = "kube-vip.io/has-ip"
|
||||
nodeLabelJSONPath = `kube-vip.io~1has-ip`
|
||||
)
|
||||
|
||||
type patchStringLabel struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
@@ -35,8 +31,8 @@ func applyNodeLabel(clientSet *kubernetes.Clientset, address, id, identity strin
|
||||
|
||||
log.Debug(fmt.Sprintf("node %s labels: %+v", id, node.Labels))
|
||||
|
||||
value, ok := node.Labels[nodeLabelIndex]
|
||||
path := fmt.Sprintf("/metadata/labels/%s", nodeLabelJSONPath)
|
||||
value, ok := node.Labels[kubevip.HasIP]
|
||||
path := fmt.Sprintf("/metadata/labels/%s", kubevip.HasIPJSONPath)
|
||||
log.Debug(fmt.Sprintf("Received identity: %s - id: %s", identity, id))
|
||||
if ok && value == address {
|
||||
log.Debug(fmt.Sprintf("removing node label `has-ip=%s` on %s", address, id))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -57,7 +57,8 @@ func (sm *Manager) annotationsWatcher() error {
|
||||
// they're as needed
|
||||
log.Warn(err.Error())
|
||||
|
||||
rw, err := watchtools.NewRetryWatcher(node.ResourceVersion, &cache.ListWatch{
|
||||
// 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)
|
||||
},
|
||||
@@ -150,8 +151,8 @@ func (sm *Manager) annotationsWatcher() error {
|
||||
// * `<info>` is the relevant information, such as `node-asn` or `peer-ip`
|
||||
// * `{{n}}` is the number of the peer, always starting with `0`
|
||||
// * kube-vip is only designed to manage one peer, just look for {{n}} == 0
|
||||
func parseBgpAnnotations(bgpConfig bgp.Config, node *v1.Node, prefix string) (bgp.Config, bgp.Peer, error) {
|
||||
bgpPeer := bgp.Peer{}
|
||||
func parseBgpAnnotations(bgpConfig kubevip.BGPConfig, node *v1.Node, prefix string) (kubevip.BGPConfig, kubevip.BGPPeer, error) {
|
||||
bgpPeer := kubevip.BGPPeer{}
|
||||
|
||||
nodeASN := ""
|
||||
for k, v := range node.Annotations {
|
||||
@@ -215,7 +216,7 @@ func parseBgpAnnotations(bgpConfig bgp.Config, node *v1.Node, prefix string) (bg
|
||||
|
||||
peerIPs := strings.Split(peerIPString, ",")
|
||||
|
||||
bgpConfig.Peers = make([]bgp.Peer, 0, len(peerIPs))
|
||||
bgpConfig.Peers = make([]kubevip.BGPPeer, 0, len(peerIPs))
|
||||
for _, peerIP := range peerIPs {
|
||||
ipAddr := strings.TrimSpace(peerIP)
|
||||
|
||||
|
||||
@@ -1,527 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/pkg/errors"
|
||||
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(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.rwClientSet.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, _ *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 *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.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 *endpointsProvider) getLabel() string {
|
||||
return ep.label
|
||||
}
|
||||
|
||||
func (ep *endpointsProvider) getProtocol() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (sm *Manager) watchEndpoint(svcCtx *serviceContext, id string, service *v1.Service, provider epProvider) 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
|
||||
leaderContext, cancel := context.WithCancel(svcCtx.ctx)
|
||||
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 <-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 <-sm.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()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if sm.config.EnableRoutingTable {
|
||||
instance := sm.findServiceInstance(service)
|
||||
if err != nil {
|
||||
log.Error("failed to find the instance", "service", service.UID, "provider", provider.getLabel(), "err", err)
|
||||
}
|
||||
if instance == nil {
|
||||
log.Error("failed to find the instance", "service", service.UID, "provider", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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[egressIPv6] == "true" && net.ParseIP(endpoints[0]).To4() != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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, and set another endpoint as last known good
|
||||
if !stillExists {
|
||||
if sm.config.EnableRoutingTable {
|
||||
if err := sm.TeardownEgress(lastKnownGoodEndpoint, service.Spec.LoadBalancerIP,
|
||||
service.Namespace, service.Annotations); err != nil {
|
||||
log.Warn("removing redundant egress rules", "err", err)
|
||||
}
|
||||
}
|
||||
if leaderElectionActive && (sm.config.EnableServicesElection || sm.config.EnableLeaderElection) {
|
||||
log.Warn(" existing endpoint has been removed, restarting leaderElection", "provider", 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]
|
||||
}
|
||||
} else {
|
||||
lastKnownGoodEndpoint = endpoints[0]
|
||||
}
|
||||
|
||||
if !leaderElectionActive && sm.config.EnableServicesElection {
|
||||
go func() {
|
||||
leaderContext, cancel = context.WithCancel(svcCtx.ctx)
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
leaderElectionActive = false
|
||||
} else {
|
||||
leaderElectionActive = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// There are local endpoints available on the node
|
||||
if !sm.config.EnableServicesElection && !sm.config.EnableLeaderElection {
|
||||
// If routing table mode is enabled - routes should be added per node
|
||||
if sm.config.EnableRoutingTable {
|
||||
instance := sm.findServiceInstance(service)
|
||||
if instance != nil {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
if !svcCtx.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", provider.getLabel(), err)
|
||||
}
|
||||
if isUpdated {
|
||||
log.Info("updated route", "provider",
|
||||
provider.getLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace", service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", sm.config.RoutingTableID)
|
||||
} else {
|
||||
log.Info("route already present", "provider",
|
||||
provider.getLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace", service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", sm.config.RoutingTableID)
|
||||
}
|
||||
} else {
|
||||
// If other error occurs, return error
|
||||
return fmt.Errorf("[%s] error adding route: %s", provider.getLabel(), err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Info("added route", "provider",
|
||||
provider.getLabel(), "ip", cluster.Network[i].IP(), "service name", service.Name, "namespace", service.Namespace, "interface", cluster.Network[i].Interface(), "tableID", sm.config.RoutingTableID)
|
||||
svcCtx.configuredNetworks.Store(cluster.Network[i].IP(), cluster.Network[i])
|
||||
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 {
|
||||
if !svcCtx.isNetworkConfigured(cluster.Network[i].IP()) {
|
||||
network := cluster.Network[i]
|
||||
if err != nil {
|
||||
log.Error("error formatting address with subnet mask", "err", err)
|
||||
}
|
||||
log.Debug("attempting to advertise BGP service", "provider", provider.getLabel(), "ip", network.CIDR())
|
||||
err = sm.bgpServer.AddHost(network.CIDR())
|
||||
if err != nil {
|
||||
log.Error("error adding BGP host", "provider", provider.getLabel(), "err", err)
|
||||
} else {
|
||||
log.Info("added BGP host", "provider",
|
||||
provider.getLabel(), "ip", network.CIDR(), "service name", service.Name, "namespace", service.Namespace)
|
||||
svcCtx.configuredNetworks.Store(cluster.Network[i].IP(), cluster.Network[i])
|
||||
leaderElectionActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There are no local endpoints
|
||||
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 {
|
||||
svcCtx.configuredNetworks.Clear()
|
||||
} else {
|
||||
for _, err := range errs {
|
||||
log.Error("error while clearing routes", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
network := cluster.Network[i]
|
||||
err = sm.bgpServer.DelHost(network.CIDR())
|
||||
if err != nil {
|
||||
log.Error("[endpoint] deleting BGP host", "provider", provider.getLabel(), "ip", network.CIDR(), "err", err)
|
||||
} else {
|
||||
log.Info("[endpoint] deleted BGP host", "provider",
|
||||
provider.getLabel(), "ip", network.CIDR(), "service name", service.Name, "namespace", service.Namespace)
|
||||
|
||||
svcCtx.configuredNetworks.Delete(cluster.Network[i].IP())
|
||||
leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no local endpoints, and we had one then remove it and stop the leaderElection
|
||||
if lastKnownGoodEndpoint != "" {
|
||||
log.Warn("existing endpoint has been removed, no remaining endpoints for leaderElection", "provider", provider.getLabel(), "endpoint", lastKnownGoodEndpoint)
|
||||
if err := sm.TeardownEgress(lastKnownGoodEndpoint, service.Spec.LoadBalancerIP, service.Namespace, service.Annotations); err != nil {
|
||||
log.Error("error removing redundant egress rules", "err", err)
|
||||
}
|
||||
|
||||
lastKnownGoodEndpoint = "" // reset endpoint
|
||||
if sm.config.EnableServicesElection || sm.config.EnableLeaderElection {
|
||||
cancel() // stop services watcher
|
||||
}
|
||||
leaderElectionActive = false
|
||||
}
|
||||
}
|
||||
// Set the service accordingly
|
||||
if service.Annotations[egress] == "true" {
|
||||
service.Annotations[activeEndpointAnnotation] = lastKnownGoodEndpoint
|
||||
}
|
||||
|
||||
log.Debug("watcher", "provider",
|
||||
provider.getLabel(), "service name", service.Name, "namespace", service.Namespace, "endpoints", len(endpoints), "last endpoint", lastKnownGoodEndpoint, "active leader election", 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.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
|
||||
}
|
||||
|
||||
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 {
|
||||
route := cluster.Network[i].PrepareRoute()
|
||||
// check if route we are about to delete is not referenced by more than one service
|
||||
if sm.countRouteReferences(route) <= 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(), "tableID", sm.config.RoutingTableID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (sm *Manager) clearBGPHosts(service *v1.Service) {
|
||||
if instance := sm.findServiceInstance(service); instance != nil {
|
||||
sm.clearBGPHostsByInstance(instance)
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *Manager) clearBGPHostsByInstance(instance *cluster.Instance) {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
network := cluster.Network[i]
|
||||
err := sm.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (svcCtx *serviceContext) isNetworkConfigured(ip string) bool {
|
||||
_, exists := svcCtx.configuredNetworks.Load(ip)
|
||||
return exists
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
"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/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
// services keeps track of services that already were processed
|
||||
var services sync.Map
|
||||
|
||||
type serviceContext struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
isActive bool
|
||||
isWatched bool
|
||||
configuredNetworks sync.Map
|
||||
}
|
||||
|
||||
func newServiceContext(ctx context.Context) *serviceContext {
|
||||
svcCtx, svcCancel := context.WithCancel(ctx)
|
||||
return &serviceContext{
|
||||
ctx: svcCtx,
|
||||
cancel: svcCancel,
|
||||
}
|
||||
}
|
||||
|
||||
// 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) error) error {
|
||||
// first start port mirroring if enabled
|
||||
if err := sm.startTrafficMirroringIfEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
// clean up traffic mirror related config
|
||||
err := sm.stopTrafficMirroringIfEnabled()
|
||||
if err != nil {
|
||||
log.Error("Stopping traffic mirroring", "err", 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.Info("(svcs) starting services watcher for all namespaces")
|
||||
} else {
|
||||
log.Info("(svcs) starting services watcher", "namespace", 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(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return sm.rwClientSet.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
|
||||
}
|
||||
|
||||
// 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)
|
||||
break
|
||||
}
|
||||
|
||||
// Select loadbalancer class filtering function
|
||||
lbClassFilterFunc := sm.lbClassFilter
|
||||
if sm.config.LoadBalancerClassLegacyHandling {
|
||||
lbClassFilterFunc = sm.lbClassFilterLegacy
|
||||
}
|
||||
|
||||
// Check the loadBalancer class
|
||||
if lbClassFilterFunc(svc) {
|
||||
break
|
||||
}
|
||||
|
||||
svcAddresses := cluster.FetchServiceAddresses(svc)
|
||||
|
||||
// We only care about LoadBalancer services that have been allocated an address
|
||||
if len(svcAddresses) <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
svcCtx, err := getServiceContext(svc.UID)
|
||||
if err != nil {
|
||||
return 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 := sm.findServiceInstance(svc)
|
||||
originalService := []string{}
|
||||
shouldGarbageCollect := true
|
||||
if i != nil {
|
||||
originalService = cluster.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(sm.config.Interface, addr, sm.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", sm.config.Interface)
|
||||
}
|
||||
}
|
||||
}
|
||||
// This service has been modified, but it was also active.
|
||||
if svcCtx != nil && svcCtx.isActive {
|
||||
if i != nil {
|
||||
if !reflect.DeepEqual(originalService, svcAddresses) {
|
||||
|
||||
// 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 = sm.deleteService(svc.UID)
|
||||
if err != nil {
|
||||
log.Error("(svc) unable to remove", "service", svc.UID)
|
||||
}
|
||||
|
||||
services.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", cluster.FetchServiceAddresses(svc))
|
||||
|
||||
if svcCtx == nil {
|
||||
svcCtx = newServiceContext(ctx)
|
||||
services.Store(svc.UID, svcCtx)
|
||||
}
|
||||
|
||||
if sm.config.EnableServicesElection || // Service Election
|
||||
((sm.config.EnableRoutingTable || sm.config.EnableBGP) && // Routing table mode or BGP
|
||||
(!sm.config.EnableLeaderElection && !sm.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 (sm.config.EnableRoutingTable || sm.config.EnableBGP) && (!sm.config.EnableLeaderElection && !sm.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 epProvider
|
||||
if !sm.config.EnableEndpointSlices {
|
||||
provider = &endpointsProvider{label: "endpoints"}
|
||||
} else {
|
||||
provider = &endpointslicesProvider{label: "endpointslices"}
|
||||
}
|
||||
if err = sm.watchEndpoint(svcCtx, sm.config.NodeName, svc, provider); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// We're now watching this service
|
||||
svcCtx.isWatched = true
|
||||
}
|
||||
} else if (sm.config.EnableBGP || sm.config.EnableRoutingTable) && (!sm.config.EnableLeaderElection && !sm.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 epProvider
|
||||
if !sm.config.EnableEndpointSlices {
|
||||
provider = &endpointsProvider{label: "endpoints"}
|
||||
} else {
|
||||
provider = &endpointslicesProvider{label: "endpointslices"}
|
||||
}
|
||||
if err = sm.watchEndpoint(svcCtx, sm.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
|
||||
}
|
||||
case watch.Deleted:
|
||||
svc, ok := event.Object.(*v1.Service)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to parse Kubernetes services from API watcher")
|
||||
}
|
||||
svcCtx, err := getServiceContext(svc.UID)
|
||||
if err != nil {
|
||||
return 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 {
|
||||
break
|
||||
}
|
||||
|
||||
// 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)
|
||||
break
|
||||
}
|
||||
|
||||
// If no leader election is enabled, delete routes here
|
||||
if !sm.config.EnableLeaderElection && !sm.config.EnableServicesElection &&
|
||||
sm.config.EnableRoutingTable && svcCtx.hasConfiguredNetworks() {
|
||||
if errs := sm.clearRoutes(svc); len(errs) == 0 {
|
||||
svcCtx.configuredNetworks.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an active service then and additional leaderElection will handle stopping
|
||||
err = sm.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()
|
||||
services.Delete(svc.UID)
|
||||
}
|
||||
|
||||
if sm.config.EnableLeaderElection && !sm.config.EnableServicesElection {
|
||||
if sm.config.EnableBGP {
|
||||
sm.clearBGPHosts(svc)
|
||||
} else if sm.config.EnableRoutingTable {
|
||||
sm.clearRoutes(svc)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("(svcs) deleted", "service name", svc.Name, "namespace", svc.Namespace)
|
||||
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 (sm *Manager) lbClassFilterLegacy(svc *v1.Service) 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 != sm.config.LoadBalancerClassName {
|
||||
log.Info("(svcs) specified the wrong loadBalancer class", "service name", svc.Name, "lbClass", *svc.Spec.LoadBalancerClass)
|
||||
return true
|
||||
}
|
||||
} 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.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 (sm *Manager) lbClassFilter(svc *v1.Service) bool {
|
||||
if svc == nil {
|
||||
log.Info("(svcs) service is nil, ignoring")
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass == nil && sm.config.LoadBalancerClassName != "" {
|
||||
log.Info("(svcs) no loadBalancer class, ignoring", "service name", svc.Name, "expected lbClass", sm.config.LoadBalancerClassName)
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass == nil && sm.config.LoadBalancerClassName == "" {
|
||||
return false
|
||||
}
|
||||
if *svc.Spec.LoadBalancerClass != sm.config.LoadBalancerClassName {
|
||||
log.Info("(svcs) specified wrong loadBalancer class, ignoring", "service name", svc.Name, "wrong lbClass", *svc.Spec.LoadBalancerClass, "expected lbClass", sm.config.LoadBalancerClassName)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (svcCtx *serviceContext) hasConfiguredNetworks() bool {
|
||||
cnt := 0
|
||||
svcCtx.configuredNetworks.Range(func(_ any, _ any) bool {
|
||||
cnt++
|
||||
return cnt < 1
|
||||
})
|
||||
return cnt > 0
|
||||
}
|
||||
|
||||
func getServiceContext(uid types.UID) (*serviceContext, error) {
|
||||
svcCtx, ok := services.Load(uid)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, ok := svcCtx.(*serviceContext)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cast service context pointer - UID: %s", uid)
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -15,7 +15,7 @@ func TestParseBgpAnnotations(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Annotations: map[string]string{}},
|
||||
}
|
||||
|
||||
bgpConfigBase := bgp.Config{
|
||||
bgpConfigBase := kubevip.BGPConfig{
|
||||
HoldTime: 15,
|
||||
KeepaliveInterval: 5,
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func TestParseBgpAnnotations(t *testing.T) {
|
||||
t.Fatal("Parsing BGP annotations should return nil when minimum config is met")
|
||||
}
|
||||
|
||||
bgpPeers := []bgp.Peer{
|
||||
bgpPeers := []kubevip.BGPPeer{
|
||||
{Address: "10.0.0.1", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.2", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.3", AS: uint32(64000), Password: "password"},
|
||||
@@ -77,7 +77,7 @@ func TestParseNewBgpAnnotations(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test", Annotations: map[string]string{}},
|
||||
}
|
||||
|
||||
bgpConfigBase := bgp.Config{
|
||||
bgpConfigBase := kubevip.BGPConfig{
|
||||
HoldTime: 15,
|
||||
KeepaliveInterval: 5,
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestParseNewBgpAnnotations(t *testing.T) {
|
||||
t.Fatalf("Parsing BGP annotations should return nil when minimum config is met [%v]", err)
|
||||
}
|
||||
|
||||
bgpPeers := []bgp.Peer{
|
||||
bgpPeers := []kubevip.BGPPeer{
|
||||
{Address: "10.0.0.1", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.2", AS: uint32(64000), Password: "password"},
|
||||
{Address: "10.0.0.3", AS: uint32(64000), Password: "password"},
|
||||
@@ -121,15 +121,15 @@ func Test_parseBgpAnnotations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bgp.Config
|
||||
want1 bgp.Peer
|
||||
want kubevip.BGPConfig
|
||||
want1 kubevip.BGPPeer
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := parseBgpAnnotations(bgp.Config{}, tt.args.node, tt.args.prefix)
|
||||
got, got1, err := parseBgpAnnotations(kubevip.BGPConfig{}, tt.args.node, tt.args.prefix)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseBgpAnnotations() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
|
||||
486
pkg/nftables/nftables.go
Normal file
486
pkg/nftables/nftables.go
Normal file
@@ -0,0 +1,486 @@
|
||||
package nftables
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/binaryutil"
|
||||
"github.com/google/nftables/expr"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
NatTable = "kube_vip_%s"
|
||||
SNatChain = "kube_vip_snat_%s"
|
||||
)
|
||||
|
||||
func ApplySNAT(podIP, vipIP, service, destinationPorts string, ignoreCIDR []string, IPv6 bool) error {
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tableName string
|
||||
if IPv6 {
|
||||
tableName = fmt.Sprintf(NatTable, "v6")
|
||||
} else {
|
||||
tableName = fmt.Sprintf(NatTable, "v4")
|
||||
}
|
||||
// Look up the table
|
||||
if t, err := FilterTable(conn, tableName, IPv6); err != nil {
|
||||
if t == nil {
|
||||
// If it doesn't exist then create it
|
||||
slog.Debug("[egress]", "Creating Table", tableName)
|
||||
conn.AddTable(GetTable(IPv6))
|
||||
}
|
||||
}
|
||||
slog.Debug("[egress]", "Creating Chain for service", service, utils.IPv6Family, IPv6)
|
||||
conn.AddChain(GetSNatChain(IPv6, service))
|
||||
err = conn.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Create our nftables rule
|
||||
rule, err := CreateRule(podIP, vipIP, service, destinationPorts, ignoreCIDR, conn, IPv6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Debug("[egress]", "table", rule.Table.Name, "chain", rule.Chain.Name, "expr", rule.Exprs)
|
||||
conn.AddRule(rule) // Add the rule
|
||||
|
||||
err = conn.Flush() // Commit the rule to nftables
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return conn.CloseLasting() // Close out any remaining netlink communication
|
||||
}
|
||||
|
||||
func DeleteSNAT(IPv6 bool, service string) error {
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var chainName = fmt.Sprintf(SNatChain, service)
|
||||
slog.Info("[egress]", "Looking for", chainName)
|
||||
|
||||
chain, err := conn.ListChain(GetTable(IPv6), chainName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chain != nil {
|
||||
slog.Info("[egress]", "Deleting chain", chainName)
|
||||
conn.DelChain(chain)
|
||||
return conn.Flush()
|
||||
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to find chain [%s]", chainName)
|
||||
}
|
||||
|
||||
func GetTable(IPv6 bool) *nftables.Table {
|
||||
var tableName string
|
||||
if IPv6 {
|
||||
tableName = fmt.Sprintf(NatTable, "v6")
|
||||
} else {
|
||||
tableName = fmt.Sprintf(NatTable, "v4")
|
||||
}
|
||||
// Default to IPv4
|
||||
table := &nftables.Table{
|
||||
Family: nftables.TableFamilyIPv4,
|
||||
Name: tableName,
|
||||
}
|
||||
|
||||
// Move to IPv6 if needed
|
||||
if IPv6 {
|
||||
table.Family = nftables.TableFamilyIPv6
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
func GetSNatChain(IPv6 bool, service string) *nftables.Chain {
|
||||
var chainName = fmt.Sprintf(SNatChain, service)
|
||||
policy := nftables.ChainPolicyAccept
|
||||
return &nftables.Chain{
|
||||
Name: chainName,
|
||||
Table: GetTable(IPv6),
|
||||
Type: nftables.ChainTypeNAT,
|
||||
Hooknum: nftables.ChainHookPostrouting,
|
||||
Priority: nftables.ChainPriorityNATSource,
|
||||
Policy: &policy,
|
||||
}
|
||||
}
|
||||
|
||||
func FilterTable(conn *nftables.Conn, tableName string, IPv6 bool) (*nftables.Table, error) {
|
||||
if IPv6 {
|
||||
return conn.ListTableOfFamily(tableName, nftables.TableFamilyIPv6)
|
||||
}
|
||||
return conn.ListTableOfFamily(tableName, nftables.TableFamilyIPv4)
|
||||
}
|
||||
|
||||
// ClearTable will remove the original tables and create new empty ones
|
||||
func ClearTable(conn *nftables.Conn) error {
|
||||
tableName := fmt.Sprintf(NatTable, "v6")
|
||||
if t, err := FilterTable(conn, tableName, false); err != nil {
|
||||
return err
|
||||
} else if t != nil {
|
||||
conn.DelTable(t)
|
||||
}
|
||||
|
||||
// These don't return errors, so not 100% sure how to guarantee things were created
|
||||
conn.AddTable(GetTable(true))
|
||||
tableName = fmt.Sprintf(NatTable, "v4")
|
||||
if t, err := FilterTable(conn, tableName, true); err != nil {
|
||||
return err
|
||||
} else if t != nil {
|
||||
conn.DelTable(t)
|
||||
}
|
||||
|
||||
// These don't return errors, so not 100% sure how to guarantee things were created
|
||||
conn.AddTable(GetTable(false))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create our nftables rule
|
||||
func CreateRule(podIP, vipIP, service, destinationPorts string, ignoreCIDR []string, conn *nftables.Conn, IPv6 bool) (*nftables.Rule, error) {
|
||||
|
||||
// Validate pod IP
|
||||
if net.ParseIP(podIP) == nil {
|
||||
return nil, errors.New("ip is invalid")
|
||||
}
|
||||
|
||||
// Validate vip IP
|
||||
if net.ParseIP(vipIP) == nil {
|
||||
return nil, errors.New("output_ip is not a valid ip")
|
||||
}
|
||||
|
||||
// Get the kube-vip table
|
||||
table := GetTable(IPv6)
|
||||
|
||||
// Create our rule
|
||||
rule := &nftables.Rule{
|
||||
Table: table,
|
||||
Exprs: []expr.Any{},
|
||||
}
|
||||
// Set the correct chain
|
||||
rule.Chain = GetSNatChain(IPv6, service)
|
||||
|
||||
// Create a set for our original/source address
|
||||
set := &nftables.Set{
|
||||
Table: table,
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
KeyType: nftables.TypeIPAddr,
|
||||
Interval: false,
|
||||
}
|
||||
if IPv6 {
|
||||
set.KeyType = nftables.TypeIP6Addr
|
||||
} else {
|
||||
set.KeyType = nftables.TypeIPAddr
|
||||
}
|
||||
|
||||
// Create an element using our pod IP
|
||||
elements := []nftables.SetElement{}
|
||||
if IPv6 {
|
||||
elements = append(elements, nftables.SetElement{Key: net.ParseIP(podIP).To16()})
|
||||
} else {
|
||||
elements = append(elements, nftables.SetElement{Key: net.ParseIP(podIP).To4()})
|
||||
}
|
||||
|
||||
// Add the elements to the set
|
||||
err := conn.AddSet(set, elements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the expression using the set
|
||||
expression := []expr.Any{}
|
||||
|
||||
payload := &expr.Payload{
|
||||
OperationType: expr.PayloadLoad,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
DestRegister: 1,
|
||||
SourceRegister: 0,
|
||||
}
|
||||
|
||||
// Set the length of the data based upon the type of IP version being used
|
||||
if IPv6 {
|
||||
payload.Offset = 8
|
||||
payload.Len = 16
|
||||
} else {
|
||||
payload.Offset = 12
|
||||
payload.Len = 4
|
||||
}
|
||||
lookup := &expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
DestRegister: 0,
|
||||
SetID: set.ID,
|
||||
}
|
||||
|
||||
// Add expressions
|
||||
expression = append(expression, payload)
|
||||
expression = append(expression, lookup)
|
||||
|
||||
// Add expression to the rule
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
|
||||
// If we filter on ports protocols then parse them
|
||||
if destinationPorts != "" {
|
||||
fixedPorts := strings.Split(destinationPorts, ",")
|
||||
|
||||
// Create an element using our pod IP
|
||||
tcpElements := []nftables.SetElement{}
|
||||
udpElements := []nftables.SetElement{}
|
||||
sctpElements := []nftables.SetElement{}
|
||||
|
||||
tcpSet := &nftables.Set{
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
Table: table,
|
||||
KeyType: nftables.TypeInetService,
|
||||
}
|
||||
udpSet := &nftables.Set{
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
Table: table,
|
||||
KeyType: nftables.TypeInetService,
|
||||
}
|
||||
sctpSet := &nftables.Set{
|
||||
Anonymous: true,
|
||||
Constant: true,
|
||||
Table: table,
|
||||
KeyType: nftables.TypeInetService,
|
||||
}
|
||||
for _, fixedPort := range fixedPorts {
|
||||
data := strings.Split(fixedPort, ":")
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
} else if len(data) == 2 { // Ensure we have two elements { proto:port }
|
||||
// parse the port to a number
|
||||
port, err := strconv.Atoi(data[1])
|
||||
if err != nil {
|
||||
slog.Error("[egress]", "unable to process port", data[1])
|
||||
continue
|
||||
}
|
||||
// Ensure the port is within the valid range for uint16
|
||||
if port < 0 || port > 65535 {
|
||||
slog.Error("[egress]", "port out of range for uint16", data[1])
|
||||
continue
|
||||
}
|
||||
|
||||
switch data[0] {
|
||||
case "tcp":
|
||||
//nolint:gosec
|
||||
tcpElements = append(tcpElements, nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(uint16(port))})
|
||||
case "udp":
|
||||
//nolint:gosec
|
||||
udpElements = append(udpElements, nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(uint16(port))})
|
||||
case "sctp":
|
||||
//nolint:gosec
|
||||
sctpElements = append(sctpElements, nftables.SetElement{Key: binaryutil.BigEndian.PutUint16(uint16(port))})
|
||||
default:
|
||||
slog.Error("[egress]", "unknown protocol", data[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add TCP Ports
|
||||
if len(tcpElements) != 0 {
|
||||
err = conn.AddSet(tcpSet, tcpElements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
// [ cmp eq reg 1 0x00000006 ]
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{unix.IPPROTO_TCP},
|
||||
},
|
||||
|
||||
// [ payload load 2b @ transport header + 2 => reg 1 ]
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
// [ lookup reg 1 set __set%d ]
|
||||
&expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
SetName: tcpSet.Name,
|
||||
SetID: tcpSet.ID,
|
||||
},
|
||||
}
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
|
||||
// Add UDP ports
|
||||
if len(udpElements) != 0 {
|
||||
err = conn.AddSet(udpSet, udpElements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
// [ cmp eq reg 1 0x00000006 ]
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{unix.IPPROTO_UDP},
|
||||
},
|
||||
|
||||
// [ payload load 2b @ transport header + 2 => reg 1 ]
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
// [ lookup reg 1 set __set%d ]
|
||||
&expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
SetName: udpSet.Name,
|
||||
SetID: udpSet.ID,
|
||||
},
|
||||
}
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
|
||||
// Add SCTP Ports
|
||||
if len(sctpElements) != 0 {
|
||||
err = conn.AddSet(sctpSet, sctpElements)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression := []expr.Any{
|
||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||
// [ cmp eq reg 1 0x00000006 ]
|
||||
&expr.Cmp{
|
||||
Op: expr.CmpOpEq,
|
||||
Register: 1,
|
||||
Data: []byte{unix.IPPROTO_SCTP},
|
||||
},
|
||||
|
||||
// [ payload load 2b @ transport header + 2 => reg 1 ]
|
||||
&expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseTransportHeader,
|
||||
Offset: 2,
|
||||
Len: 2,
|
||||
},
|
||||
// [ lookup reg 1 set __set%d ]
|
||||
&expr.Lookup{
|
||||
SourceRegister: 1,
|
||||
SetName: sctpSet.Name,
|
||||
SetID: sctpSet.ID,
|
||||
},
|
||||
}
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse which CIDRs we will not SNAT for
|
||||
for _, cidr := range ignoreCIDR {
|
||||
start, end, err := nftables.NetFirstAndLastIP(cidr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expression = []expr.Any{}
|
||||
|
||||
payload := &expr.Payload{
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
}
|
||||
notEqualRange := &expr.Range{
|
||||
Op: expr.CmpOpNeq,
|
||||
Register: 1,
|
||||
}
|
||||
|
||||
if IPv6 {
|
||||
payload.Len = 16
|
||||
payload.Offset = 24
|
||||
notEqualRange.FromData = start.To16()
|
||||
notEqualRange.ToData = end.To16()
|
||||
} else {
|
||||
payload.Offset = 16
|
||||
payload.Len = 4
|
||||
notEqualRange.FromData = start.To4()
|
||||
notEqualRange.ToData = end.To4()
|
||||
}
|
||||
// Add expressions
|
||||
expression = append(expression, payload)
|
||||
expression = append(expression, notEqualRange)
|
||||
|
||||
// // Add expression to the rule
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
}
|
||||
|
||||
// Final expression to the rule is the SNAT to the VIP address
|
||||
expression = []expr.Any{}
|
||||
|
||||
immediate := &expr.Immediate{
|
||||
Register: 1,
|
||||
}
|
||||
|
||||
nat := &expr.NAT{
|
||||
Type: expr.NATTypeSourceNAT,
|
||||
RegAddrMin: 1,
|
||||
RegAddrMax: 1,
|
||||
RegProtoMin: 0,
|
||||
RegProtoMax: 0,
|
||||
Random: false,
|
||||
FullyRandom: false,
|
||||
Persistent: false,
|
||||
Prefix: false,
|
||||
}
|
||||
|
||||
if IPv6 {
|
||||
immediate.Data = net.ParseIP(vipIP).To16()
|
||||
nat.Family = unix.NFPROTO_IPV6
|
||||
} else {
|
||||
immediate.Data = net.ParseIP(vipIP).To4()
|
||||
nat.Family = unix.NFPROTO_IPV4
|
||||
}
|
||||
// https://github.com/google/nftables/blob/main/nftables_test.go#L5375
|
||||
// Add expressions
|
||||
expression = append(expression, immediate)
|
||||
expression = append(expression, nat)
|
||||
rule.Exprs = append(rule.Exprs, expression...)
|
||||
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
// Returns a list of all chains IPv4/IPv6 in nftables
|
||||
func ListChains() ([]string, error) {
|
||||
chains := []string{}
|
||||
conn, err := nftables.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipv4, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipv6, err := conn.ListChainsOfTableFamily(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for x := range ipv4 {
|
||||
chains = append(chains, fmt.Sprintf("Table=%s, Chain=%s", ipv4[x].Table.Name, ipv4[x].Name))
|
||||
}
|
||||
for x := range ipv6 {
|
||||
chains = append(chains, fmt.Sprintf("Table=%s, Chain=%s", ipv6[x].Table.Name, ipv6[x].Name))
|
||||
}
|
||||
|
||||
_ = conn.CloseLasting() // TODO: Should we ignore this error, we're not actually doing any actions with nftables
|
||||
return chains, nil
|
||||
}
|
||||
164
pkg/node/labeler/label_manager.go
Normal file
164
pkg/node/labeler/label_manager.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package labeler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
// labelName is the name of the label that will be added to the node
|
||||
// it is prefix for the label key before "/"
|
||||
labelName = "service-provided.kube-vip.io"
|
||||
)
|
||||
|
||||
// labelOperation is the operation to perform on the node labels
|
||||
type labelOperation string
|
||||
|
||||
// labelOperation constants
|
||||
const (
|
||||
labelOperationRemove labelOperation = "remove"
|
||||
labelOperationAdd labelOperation = "add"
|
||||
)
|
||||
|
||||
// NewManager creates a new Label Manager for the given node
|
||||
func NewManager(nodeName string, clientSet *kubernetes.Clientset) *Manager {
|
||||
return &Manager{
|
||||
nodeName: nodeName,
|
||||
clientSet: clientSet,
|
||||
}
|
||||
}
|
||||
|
||||
// Manager is the label Manager for the node
|
||||
type Manager struct {
|
||||
// nodeName is the name of the node to manage
|
||||
nodeName string
|
||||
|
||||
// clientSet is the Kubernetes client set to use
|
||||
clientSet *kubernetes.Clientset
|
||||
}
|
||||
|
||||
// AddLabel a new label to the node
|
||||
func (m *Manager) AddLabel(ctx context.Context, svc *corev1.Service) error {
|
||||
log.Debug("[service] add label to node", "namespace", svc.Namespace, "name", svc.Name)
|
||||
labelKey, labelValue := generateNodeLabelKeyValue(svc)
|
||||
return m.patchNode(ctx, labelOperationAdd, map[string]string{labelKey: labelValue})
|
||||
}
|
||||
|
||||
// RemoveLabel a label from the node
|
||||
func (m *Manager) RemoveLabel(ctx context.Context, svc *corev1.Service) error {
|
||||
log.Debug("[service] delete label from node", "namespace", svc.Namespace, "name", svc.Name)
|
||||
labelKey, _ := generateNodeLabelKeyValue(svc)
|
||||
return m.patchNode(ctx, labelOperationRemove, map[string]string{labelKey: ""})
|
||||
}
|
||||
|
||||
// clean up the node labels
|
||||
func (m *Manager) CleanUpLabels(timeout time.Duration) error {
|
||||
log.Debug("cleaning up labels for node", "node", m.nodeName, "timeout", timeout)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// get the node
|
||||
node, err := m.clientSet.CoreV1().Nodes().Get(ctx, m.nodeName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get node %s", m.nodeName)
|
||||
}
|
||||
|
||||
// collect all labels with the prefix to remove
|
||||
labels := map[string]string{}
|
||||
for k := range node.Labels {
|
||||
if strings.HasPrefix(k, labelName) {
|
||||
labels[k] = ""
|
||||
}
|
||||
}
|
||||
|
||||
if len(labels) == 0 {
|
||||
log.Debug("no labels to remove for node", "node", m.nodeName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// patch the node with the labels to remove
|
||||
return m.patchNode(ctx, labelOperationRemove, labels)
|
||||
}
|
||||
|
||||
// generateNodeLabelKeyValue generates a label key and value for the given service
|
||||
func generateNodeLabelKeyValue(svc *corev1.Service) (string, string) {
|
||||
addresses, _ := instance.FetchServiceAddresses(svc)
|
||||
|
||||
sanitized := make([]string, len(addresses))
|
||||
for i, addr := range addresses {
|
||||
sanitized[i] = sanitizeIPForLabel(addr)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s.%s", labelName, svc.Name, svc.Namespace), strings.Join(sanitized, ",")
|
||||
}
|
||||
|
||||
// Helper function to convert IPv6 hex address without colons
|
||||
func sanitizeIPForLabel(addr string) string {
|
||||
ip := net.ParseIP(addr)
|
||||
if ip == nil || ip.To4() != nil {
|
||||
return addr
|
||||
}
|
||||
return hex.EncodeToString(ip.To16())
|
||||
}
|
||||
|
||||
// patchNode patches the node with the given labels
|
||||
func (m *Manager) patchNode(ctx context.Context, operation labelOperation, labels map[string]string) error {
|
||||
type patchStringLabel struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
patchLabels := []patchStringLabel{}
|
||||
// generate the patch
|
||||
for k, v := range labels {
|
||||
patchLabels = append(patchLabels, patchStringLabel{
|
||||
Op: string(operation),
|
||||
// replace all slashes with ~1
|
||||
Path: fmt.Sprintf("/metadata/labels/%s", strings.ReplaceAll(k, "/", "~1")),
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
patchData, err := json.Marshal(patchLabels)
|
||||
if err != nil {
|
||||
log.Debug("node patch marshaling failed", "err", err, "labels", labels, "patch", patchLabels)
|
||||
return errors.Wrapf(err, "node patch marshaling failed for labels %v", labels)
|
||||
}
|
||||
|
||||
log.Debug("patching node",
|
||||
"node", m.nodeName,
|
||||
"patch", string(patchData),
|
||||
"operation", operation,
|
||||
"labels", labels,
|
||||
"clientSetNil", m.clientSet == nil)
|
||||
if m.clientSet == nil {
|
||||
return errors.New("kubernetes client is not initialized")
|
||||
}
|
||||
|
||||
// patch node
|
||||
node, err := m.clientSet.CoreV1().Nodes().Patch(ctx, m.nodeName, types.JSONPatchType, patchData, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
log.Debug("node patching failed", "err", err, "patchData", patchData)
|
||||
return errors.Wrapf(err, "node patching failed with patch %s", string(patchData))
|
||||
}
|
||||
|
||||
log.Debug("updated", "node", m.nodeName, "labels", node.Labels)
|
||||
|
||||
return nil
|
||||
}
|
||||
27
pkg/node/noop/label_noop_manager.go
Normal file
27
pkg/node/noop/label_noop_manager.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package noop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// NewManager creates a new NoOp label manager
|
||||
func NewManager() *Manager {
|
||||
return &Manager{}
|
||||
}
|
||||
|
||||
type Manager struct{}
|
||||
|
||||
func (m *Manager) AddLabel(_ context.Context, _ *corev1.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) RemoveLabel(_ context.Context, _ *corev1.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CleanUpLabels(_ time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
49
pkg/node/types.go
Normal file
49
pkg/node/types.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/node/labeler"
|
||||
"github.com/kube-vip/kube-vip/pkg/node/noop"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// LabelManager is the interface for the node label manager
|
||||
type LabelManager interface {
|
||||
// AddLabel adds a label to the node for the given service
|
||||
AddLabel(ctx context.Context, svc *v1.Service) error
|
||||
|
||||
// RemoveLabel removes the label from the node for the given service
|
||||
RemoveLabel(ctx context.Context, svc *v1.Service) error
|
||||
|
||||
// CleanUpLabels removes all labels from the node
|
||||
CleanUpLabels(timeout time.Duration) error
|
||||
}
|
||||
|
||||
// NewManager creates a new Label Manager for the given node
|
||||
// NoOp implementation is returned if node labeling is disabled, or if
|
||||
// running in control plane mode, or if the client is not ready
|
||||
func NewManager(config *kubevip.Config, clientSet *kubernetes.Clientset) LabelManager {
|
||||
if !config.EnableNodeLabeling {
|
||||
return noop.NewManager()
|
||||
}
|
||||
|
||||
if config.EnableControlPlane {
|
||||
log.Debug("Skip node labeling, control plane mode enabled")
|
||||
return noop.NewManager()
|
||||
}
|
||||
|
||||
if clientSet == nil {
|
||||
log.Debug("Skip node labeling, client is not ready")
|
||||
return noop.NewManager()
|
||||
}
|
||||
|
||||
log.Debug("Node labeling enabled")
|
||||
|
||||
return labeler.NewManager(config.NodeName, clientSet)
|
||||
}
|
||||
36
pkg/servicecontext/servicecontext.go
Normal file
36
pkg/servicecontext/servicecontext.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package servicecontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
IsActive bool
|
||||
IsWatched bool
|
||||
ConfiguredNetworks sync.Map
|
||||
}
|
||||
|
||||
func New(ctx context.Context) *Context {
|
||||
svcCtx, svcCancel := context.WithCancel(ctx)
|
||||
return &Context{
|
||||
Ctx: svcCtx,
|
||||
Cancel: svcCancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *Context) HasConfiguredNetworks() bool {
|
||||
cnt := 0
|
||||
ctx.ConfiguredNetworks.Range(func(_ any, _ any) bool {
|
||||
cnt++
|
||||
return cnt < 1
|
||||
})
|
||||
return cnt > 0
|
||||
}
|
||||
|
||||
func (ctx *Context) IsNetworkConfigured(ip string) bool {
|
||||
_, exists := ctx.ConfiguredNetworks.Load(ip)
|
||||
return exists
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package services
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/iptables"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/nftables"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
@@ -21,7 +24,7 @@ const (
|
||||
defaultServiceCIDR = "10.96.0.0/12"
|
||||
)
|
||||
|
||||
func (sm *Manager) iptablesCheck() error {
|
||||
func (p *Processor) iptablesCheck() error {
|
||||
file, err := os.Open("/proc/modules")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -48,7 +51,7 @@ func (sm *Manager) iptablesCheck() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) nftablesCheck() error {
|
||||
func (p *Processor) nftablesCheck() error {
|
||||
file, err := os.Open("/proc/modules")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -75,12 +78,12 @@ func (sm *Manager) nftablesCheck() error {
|
||||
|
||||
func getSameFamilyCidr(sourceCidrs, ip string) string { //Todo: not sure how this ever worked
|
||||
cidrs := strings.Split(sourceCidrs, ",")
|
||||
isV6 := vip.IsIPv6(ip)
|
||||
isV6 := utils.IsIPv6(ip)
|
||||
matchingFamily := []string{}
|
||||
for _, cidr := range cidrs {
|
||||
// Is the ip an IPv6 address
|
||||
if isV6 {
|
||||
if vip.IsIPv6CIDR(cidr) {
|
||||
if utils.IsIPv6CIDR(cidr) {
|
||||
matchingFamily = append(matchingFamily, cidr)
|
||||
selectedCIDR, err := checkCIDR(ip, cidr)
|
||||
if err != nil {
|
||||
@@ -92,7 +95,7 @@ func getSameFamilyCidr(sourceCidrs, ip string) string { //Todo: not sure how thi
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if vip.IsIPv4CIDR(cidr) {
|
||||
if utils.IsIPv4CIDR(cidr) {
|
||||
matchingFamily = append(matchingFamily, cidr)
|
||||
selectedCidr, err := checkCIDR(ip, cidr)
|
||||
if err != nil {
|
||||
@@ -132,26 +135,27 @@ func checkCIDR(ip, cidr string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (sm *Manager) configureEgress(vipIP, podIP, namespace string, annotations map[string]string) error {
|
||||
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[egressDestinationPorts]
|
||||
deniedNetworks := annotations[egressDeniedNetworks]
|
||||
allowedNetworks := annotations[egressAllowedNetworks]
|
||||
destinationPorts := annotations[kubevip.EgressDestinationPorts]
|
||||
deniedNetworks := annotations[kubevip.EgressDeniedNetworks]
|
||||
allowedNetworks := annotations[kubevip.EgressAllowedNetworks]
|
||||
internalEgress := annotations[kubevip.EgressInternal]
|
||||
|
||||
if sm.config.EgressPodCidr == "" || sm.config.EgressServiceCidr == "" {
|
||||
autoServiceCIDR, autoPodCIDR, discoverErr = sm.AutoDiscoverCIDRs()
|
||||
if p.config.EgressPodCidr == "" || p.config.EgressServiceCidr == "" {
|
||||
autoServiceCIDR, autoPodCIDR, discoverErr = p.AutoDiscoverCIDRs()
|
||||
}
|
||||
|
||||
if discoverErr != nil {
|
||||
log.Warn("autodiscover CIDR", "err", discoverErr)
|
||||
}
|
||||
|
||||
if sm.config.EgressPodCidr != "" {
|
||||
podCidr = getSameFamilyCidr(sm.config.EgressPodCidr, podIP)
|
||||
if p.config.EgressPodCidr != "" {
|
||||
podCidr = getSameFamilyCidr(p.config.EgressPodCidr, podIP)
|
||||
} else {
|
||||
if discoverErr == nil {
|
||||
podCidr = getSameFamilyCidr(autoPodCIDR, podIP)
|
||||
@@ -160,14 +164,14 @@ func (sm *Manager) configureEgress(vipIP, podIP, namespace string, annotations m
|
||||
|
||||
if podCidr == "" {
|
||||
// There's no default IPv6 pod CIDR, therefore we silently back off if CIDR s not specified.
|
||||
if !vip.IsIPv4(podIP) {
|
||||
if !utils.IsIPv4(podIP) {
|
||||
return fmt.Errorf("error with the CIDR [%s]", podIP)
|
||||
}
|
||||
podCidr = defaultPodCIDR
|
||||
}
|
||||
|
||||
if sm.config.EgressServiceCidr != "" {
|
||||
serviceCidr = getSameFamilyCidr(sm.config.EgressServiceCidr, vipIP)
|
||||
if p.config.EgressServiceCidr != "" {
|
||||
serviceCidr = getSameFamilyCidr(p.config.EgressServiceCidr, vipIP)
|
||||
} else {
|
||||
if discoverErr == nil {
|
||||
serviceCidr = getSameFamilyCidr(autoServiceCIDR, vipIP)
|
||||
@@ -176,7 +180,7 @@ func (sm *Manager) configureEgress(vipIP, podIP, namespace string, annotations m
|
||||
|
||||
if serviceCidr == "" {
|
||||
// There's no default IPv6 service CIDR, therefore we silently back off if CIDR s not specified.
|
||||
if !vip.IsIPv4(vipIP) {
|
||||
if !utils.IsIPv4(vipIP) {
|
||||
return nil
|
||||
}
|
||||
serviceCidr = defaultServiceCIDR
|
||||
@@ -185,27 +189,50 @@ func (sm *Manager) configureEgress(vipIP, podIP, namespace string, annotations m
|
||||
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) {
|
||||
if utils.IsIPv4(podIP) != utils.IsIPv4CIDR(podCidr) {
|
||||
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "podCIDR", podCidr)
|
||||
return nil
|
||||
}
|
||||
|
||||
if vip.IsIPv4(vipIP) != vip.IsIPv4CIDR(serviceCidr) {
|
||||
if utils.IsIPv4(vipIP) != utils.IsIPv4CIDR(serviceCidr) {
|
||||
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "serviceCIDR", serviceCidr)
|
||||
return nil
|
||||
}
|
||||
|
||||
if vip.IsIPv4(vipIP) != vip.IsIPv4(podIP) {
|
||||
if utils.IsIPv4(vipIP) != utils.IsIPv4(podIP) {
|
||||
log.Error("[Egress] family is not matching. Backing off...", "pod", podIP, "vipIP", vipIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
protocol := iptables.ProtocolIPv4
|
||||
if vip.IsIPv6(vipIP) {
|
||||
if utils.IsIPv6(vipIP) {
|
||||
protocol = iptables.ProtocolIPv6
|
||||
}
|
||||
|
||||
i, err := vip.CreateIptablesClient(sm.config.EgressWithNftables, namespace, protocol)
|
||||
// Use the internal egress implementation
|
||||
if internalEgress != "" {
|
||||
// Create an array of CIDRs that we wont SNAT to.
|
||||
ignoreCIDRs := []string{
|
||||
podCidr,
|
||||
serviceCidr,
|
||||
}
|
||||
|
||||
// Add any specifically denied networks
|
||||
if deniedNetworks != "" {
|
||||
networks := strings.Split(strings.TrimSpace(deniedNetworks), ",") //Remove whitespace characters and then create an array from the CIDRs
|
||||
ignoreCIDRs = append(ignoreCIDRs, networks...)
|
||||
|
||||
}
|
||||
|
||||
// Apply the SNAT rules
|
||||
err := nftables.ApplySNAT(podIP, vipIP, serviceUUID, destinationPorts, ignoreCIDRs, utils.IsIPv6(vipIP))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error performing netlink nftables [%s]", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
i, err := vip.CreateIptablesClient(p.config.EgressWithNftables, namespace, protocol)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error Creating iptables client [%s]", err)
|
||||
}
|
||||
@@ -243,7 +270,7 @@ func (sm *Manager) configureEgress(vipIP, podIP, namespace string, annotations m
|
||||
}
|
||||
|
||||
mask := "/32"
|
||||
if !vip.IsIPv4(podIP) {
|
||||
if !utils.IsIPv4(podIP) {
|
||||
mask = "/128"
|
||||
}
|
||||
|
||||
@@ -304,12 +331,12 @@ func (sm *Manager) configureEgress(vipIP, podIP, namespace string, annotations m
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) AutoDiscoverCIDRs() (serviceCIDR, podCIDR string, err error) {
|
||||
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 := sm.clientSet.CoreV1().Pods("kube-system").List(context.TODO(), options)
|
||||
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)
|
||||
}
|
||||
@@ -332,83 +359,3 @@ func (sm *Manager) AutoDiscoverCIDRs() (serviceCIDR, podCIDR string, err error)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (sm *Manager) TeardownEgress(podIP, vipIP, namespace string, annotations map[string]string) error {
|
||||
// Look up the destination ports from the annotations on the service
|
||||
destinationPorts := annotations[egressDestinationPorts]
|
||||
deniedNetworks := annotations[egressDeniedNetworks]
|
||||
allowedNetworks := annotations[egressAllowedNetworks]
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package services
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
package manager
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/lease"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
)
|
||||
|
||||
var (
|
||||
svcLocks map[string]*sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
svcLocks = make(map[string]*sync.Mutex)
|
||||
}
|
||||
|
||||
// The startServicesWatchForLeaderElection function will start a services watcher, the
|
||||
func (sm *Manager) startServicesWatchForLeaderElection(ctx context.Context) error {
|
||||
err := sm.servicesWatcher(ctx, sm.StartServicesLeaderElection)
|
||||
// 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 sm.serviceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
_ = cluster.Network[i].DeleteRoute()
|
||||
if p.config.EnableRoutingTable {
|
||||
for _, instance := range p.ServiceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
for i := range cluster.Network {
|
||||
_ = cluster.Network[i].DeleteRoute()
|
||||
}
|
||||
cluster.Stop()
|
||||
}
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +38,9 @@ func (sm *Manager) startServicesWatchForLeaderElection(ctx context.Context) erro
|
||||
}
|
||||
|
||||
// The startServicesWatchForLeaderElection function will start a services watcher, the
|
||||
func (sm *Manager) 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", sm.config.NodeName)
|
||||
func (p *Processor) StartServicesLeaderElection(ctx context.Context, service *v1.Service) error {
|
||||
serviceLease, _ := lease.GetName(service)
|
||||
log.Info("new leader election", "service", service.Name, "namespace", service.Namespace, "lock_name", serviceLease, "host_id", p.config.NodeName)
|
||||
// we use the Lease lock type since edits to Leases are less common
|
||||
// and fewer objects in the cluster watch "all Leases".
|
||||
lock := &resourcelock.LeaseLock{
|
||||
@@ -54,22 +48,19 @@ func (sm *Manager) StartServicesLeaderElection(ctx context.Context, service *v1.
|
||||
Name: serviceLease,
|
||||
Namespace: service.Namespace,
|
||||
},
|
||||
Client: sm.clientSet.CoordinationV1(),
|
||||
Client: p.clientSet.CoordinationV1(),
|
||||
LockConfig: resourcelock.ResourceLockConfig{
|
||||
Identity: sm.config.NodeName,
|
||||
Identity: p.config.NodeName,
|
||||
},
|
||||
}
|
||||
childCtx, childCancel := context.WithCancel(ctx)
|
||||
defer childCancel()
|
||||
|
||||
if _, ok := svcLocks[serviceLease]; !ok {
|
||||
svcLocks[serviceLease] = new(sync.Mutex)
|
||||
}
|
||||
go func() {
|
||||
// wait for the service context to end and delete the lease then
|
||||
<-ctx.Done()
|
||||
p.leaseMgr.Delete(service)
|
||||
}()
|
||||
|
||||
svcLocks[serviceLease].Lock()
|
||||
defer svcLocks[serviceLease].Unlock()
|
||||
|
||||
svcCtx, err := getServiceContext(service.UID)
|
||||
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)
|
||||
}
|
||||
@@ -77,10 +68,42 @@ func (sm *Manager) StartServicesLeaderElection(ctx context.Context, service *v1.
|
||||
return fmt.Errorf("failed to get context for service %q with UID %q: nil context", service.Name, service.UID)
|
||||
}
|
||||
|
||||
svcCtx.isActive = true
|
||||
svcCtx.IsActive = true
|
||||
|
||||
svcLease, isNew := p.leaseMgr.Add(service)
|
||||
// this service is sharing lease
|
||||
if !isNew {
|
||||
// wait for leader election to start or context to be done
|
||||
select {
|
||||
case <-svcLease.Started:
|
||||
case <-svcLease.Ctx.Done():
|
||||
svcCtx.IsActive = false
|
||||
return nil
|
||||
}
|
||||
<-svcLease.Started
|
||||
|
||||
if lease.UsesCommon(service) {
|
||||
if err := p.SyncServices(ctx, service); err != nil {
|
||||
log.Error("service sync", "err", err)
|
||||
svcLease.Cancel()
|
||||
}
|
||||
// just block until context is cancelled
|
||||
<-ctx.Done()
|
||||
if svcCtx.IsActive {
|
||||
if err := p.deleteService(service.UID); err != nil {
|
||||
log.Error("service deletion", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wait for leaderelection to be finished
|
||||
<-svcLease.Ctx.Done()
|
||||
// Mark this service is inactive
|
||||
svcCtx.IsActive = false
|
||||
return nil
|
||||
}
|
||||
// start the leader election code loop
|
||||
leaderelection.RunOrDie(childCtx, leaderelection.LeaderElectionConfig{
|
||||
leaderelection.RunOrDie(svcLease.Ctx, leaderelection.LeaderElectionConfig{
|
||||
Lock: lock,
|
||||
// IMPORTANT: you MUST ensure that any code you have that
|
||||
// is protected by the lease must terminate **before**
|
||||
@@ -89,32 +112,33 @@ func (sm *Manager) StartServicesLeaderElection(ctx context.Context, service *v1.
|
||||
// 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,
|
||||
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 := sm.syncServices(ctx, service); err != nil {
|
||||
if err := p.SyncServices(ctx, service); err != nil {
|
||||
log.Error("service sync", "err", err)
|
||||
childCancel()
|
||||
svcLease.Cancel()
|
||||
}
|
||||
close(svcLease.Started)
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
// we can do cleanup here
|
||||
log.Info("leadership lost", "service", service.Name, "leader", sm.config.NodeName)
|
||||
if svcCtx.isActive {
|
||||
if err := sm.deleteService(service.UID); err != nil {
|
||||
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
|
||||
svcCtx.IsActive = false
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
// we're notified when new leader elected
|
||||
if identity == sm.config.NodeName {
|
||||
if identity == p.config.NodeName {
|
||||
// I just got the lock
|
||||
return
|
||||
}
|
||||
357
pkg/services/processor.go
Normal file
357
pkg/services/processor.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
log "log/slog"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/arp"
|
||||
"github.com/kube-vip/kube-vip/pkg/bgp"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/lease"
|
||||
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
config *kubevip.Config
|
||||
lbClassFilter func(svc *v1.Service, config *kubevip.Config) bool
|
||||
svcMap sync.Map
|
||||
|
||||
// Keeps track of all running instances
|
||||
ServiceInstances []*instance.Instance
|
||||
|
||||
mutex sync.Mutex
|
||||
bgpServer *bgp.Server
|
||||
|
||||
clientSet *kubernetes.Clientset
|
||||
rwClientSet *kubernetes.Clientset
|
||||
|
||||
shutdownChan chan struct{}
|
||||
|
||||
// This is a prometheus counter used to count the number of events received
|
||||
// from the service watcher
|
||||
CountServiceWatchEvent *prometheus.CounterVec
|
||||
|
||||
intfMgr *networkinterface.Manager
|
||||
arpMgr *arp.Manager
|
||||
|
||||
leaseMgr *lease.Manager
|
||||
|
||||
// nodeLabelManager is the manager for the node labels
|
||||
nodeLabelManager labelManager
|
||||
}
|
||||
|
||||
// labelManager is the interface for the node label manager to add/remove labels
|
||||
type labelManager interface {
|
||||
AddLabel(ctx context.Context, svc *v1.Service) error
|
||||
RemoveLabel(ctx context.Context, svc *v1.Service) error
|
||||
}
|
||||
|
||||
func NewServicesProcessor(config *kubevip.Config, bgpServer *bgp.Server,
|
||||
clientSet *kubernetes.Clientset, rwClientSet *kubernetes.Clientset, shutdownChan chan struct{},
|
||||
intfMgr *networkinterface.Manager, arpMgr *arp.Manager, nodeLabelManager labelManager) *Processor {
|
||||
lbClassFilterFunc := lbClassFilter
|
||||
if config.LoadBalancerClassLegacyHandling {
|
||||
lbClassFilterFunc = lbClassFilterLegacy
|
||||
}
|
||||
|
||||
return &Processor{
|
||||
config: config,
|
||||
lbClassFilter: lbClassFilterFunc,
|
||||
ServiceInstances: []*instance.Instance{},
|
||||
bgpServer: bgpServer,
|
||||
clientSet: clientSet,
|
||||
rwClientSet: rwClientSet,
|
||||
shutdownChan: shutdownChan,
|
||||
CountServiceWatchEvent: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "kube_vip",
|
||||
Subsystem: "manager",
|
||||
Name: "all_services_events",
|
||||
Help: "Count all events fired by the service watcher categorised by event type",
|
||||
}, []string{"type"}),
|
||||
|
||||
intfMgr: intfMgr,
|
||||
arpMgr: arpMgr,
|
||||
leaseMgr: lease.NewManager(),
|
||||
nodeLabelManager: nodeLabelManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) AddOrModify(ctx context.Context, event watch.Event, serviceFunc func(context.Context, *v1.Service) error) (bool, error) {
|
||||
// log.Debugf("Endpoints for service [%s] have been Created or modified", s.service.ServiceName)
|
||||
svc, ok := event.Object.(*v1.Service)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unable to parse Kubernetes services from API watcher")
|
||||
}
|
||||
|
||||
// We only care about LoadBalancer services
|
||||
if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check if we ignore this service
|
||||
if svc.Annotations[kubevip.LoadbalancerIgnore] == "true" {
|
||||
log.Info("ignore annotation for kube-vip", "service name", svc.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check the loadBalancer class
|
||||
if p.lbClassFilter(svc, p.config) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
svcAddresses, svcHostnames := instance.FetchServiceAddresses(svc)
|
||||
|
||||
// We only care about LoadBalancer services that have been allocated an address
|
||||
if len(svcAddresses) <= 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
_, usesCommonLease := svc.Annotations[kubevip.ServiceLease]
|
||||
if usesCommonLease && svc.Spec.ExternalTrafficPolicy != v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
return false, fmt.Errorf("annotation %q cannot be used with service traffic policy other than %q",
|
||||
kubevip.ServiceLease, v1.ServiceExternalTrafficPolicyTypeCluster)
|
||||
}
|
||||
|
||||
svcCtx, err := p.getServiceContext(svc.UID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get service context: %w", err)
|
||||
}
|
||||
|
||||
// The modified event should only be triggered if the service has been modified (i.e. moved somewhere else)
|
||||
if event.Type == watch.Modified {
|
||||
i := instance.FindServiceInstance(svc, p.ServiceInstances)
|
||||
shouldGarbageCollect := true
|
||||
if i != nil {
|
||||
originalServiceAddresses, originalServiceHostnames := instance.FetchServiceAddresses(i.ServiceSnapshot)
|
||||
shouldGarbageCollect =
|
||||
// Service addresses changed
|
||||
!reflect.DeepEqual(originalServiceAddresses, svcAddresses) ||
|
||||
// Service hostnames changed
|
||||
!reflect.DeepEqual(originalServiceHostnames, svcHostnames) ||
|
||||
// ExternalTrafficPolicy changed
|
||||
svc.Spec.ExternalTrafficPolicy != i.ServiceSnapshot.Spec.ExternalTrafficPolicy
|
||||
}
|
||||
if shouldGarbageCollect {
|
||||
for _, addr := range svcAddresses {
|
||||
// log.Debugf("(svcs) Retrieving local addresses, to ensure that this modified address doesn't exist: %s", addr)
|
||||
f, err := vip.GarbageCollect(p.config.Interface, addr, p.intfMgr)
|
||||
if err != nil {
|
||||
log.Error("(svcs) cleaning existing address error", "err", err)
|
||||
}
|
||||
if f {
|
||||
log.Warn("(svcs) already found existing config", "address", addr, "adapter", p.config.Interface)
|
||||
}
|
||||
}
|
||||
// This service has been modified, but it was also active.
|
||||
if svcCtx != nil && svcCtx.IsActive {
|
||||
log.Warn("(svcs) The load balancer has changed, cancelling original load balancer")
|
||||
//Set it to inactive
|
||||
svcCtx.IsActive = false
|
||||
svcCtx.Cancel()
|
||||
log.Warn("(svcs) waiting for load balancer to finish")
|
||||
<-svcCtx.Ctx.Done()
|
||||
|
||||
if err := p.deleteService(svc.UID); err != nil {
|
||||
log.Error("(svc) unable to remove", "service", svc.UID)
|
||||
}
|
||||
// in theory this should never fail
|
||||
p.svcMap.Delete(svc.UID)
|
||||
// Reset the the svcCtx when it was garbage collected
|
||||
// As the next function will create a new context when nil
|
||||
svcCtx = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Architecture walkthrough: (Had to do this as this code path is making my head hurt)
|
||||
|
||||
// Is the service active (bool), if not then process this new service
|
||||
// Does this service use an election per service?
|
||||
//
|
||||
|
||||
if svcCtx == nil || svcCtx != nil && !svcCtx.IsActive {
|
||||
ips, hostnames := instance.FetchServiceAddresses(svc)
|
||||
log.Debug("(svcs) has been added/modified with addresses", "service name", svc.Name, "ips", ips, "hostnames", hostnames)
|
||||
|
||||
if svcCtx == nil {
|
||||
svcCtx = servicecontext.New(ctx)
|
||||
p.svcMap.Store(svc.UID, svcCtx)
|
||||
}
|
||||
|
||||
if p.config.EnableServicesElection || // Service Election
|
||||
((p.config.EnableRoutingTable || p.config.EnableBGP) && // Routing table mode or BGP
|
||||
(!p.config.EnableLeaderElection && !p.config.EnableServicesElection)) { // No leaderelection or services election
|
||||
|
||||
// If this load balancer Traffic Policy is "local"
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
|
||||
|
||||
// Start an endpoint watcher if we're not watching it already
|
||||
if !svcCtx.IsWatched {
|
||||
// background the endpoint watcher
|
||||
if (p.config.EnableRoutingTable || p.config.EnableBGP) && (!p.config.EnableLeaderElection && !p.config.EnableServicesElection) {
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
|
||||
// Add Endpoint or EndpointSlices watcher
|
||||
var provider providers.Provider
|
||||
if p.config.EnableEndpoints {
|
||||
provider = providers.NewEndpoints()
|
||||
} else {
|
||||
provider = providers.NewEndpointslices()
|
||||
}
|
||||
if err = p.watchEndpoint(svcCtx, p.config.NodeName, svc, provider); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// We're now watching this service
|
||||
svcCtx.IsWatched = true
|
||||
}
|
||||
} else if (p.config.EnableBGP || p.config.EnableRoutingTable) && (!p.config.EnableLeaderElection && !p.config.EnableServicesElection) {
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeCluster {
|
||||
// Add Endpoint watcher
|
||||
var provider providers.Provider
|
||||
if p.config.EnableEndpoints {
|
||||
provider = providers.NewEndpoints()
|
||||
} else {
|
||||
provider = providers.NewEndpointslices()
|
||||
}
|
||||
if err = p.watchEndpoint(svcCtx, p.config.NodeName, svc, provider); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}()
|
||||
// We're now watching this service
|
||||
svcCtx.IsWatched = true
|
||||
} else {
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-svcCtx.Ctx.Done():
|
||||
log.Warn("(svcs) restartable service watcher ending", "uid", svc.UID)
|
||||
return
|
||||
default:
|
||||
log.Info("(svcs) restartable service watcher starting", "uid", svc.UID)
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
// Increment the waitGroup before the service Func is called (Done is completed in there)
|
||||
err = serviceFunc(svcCtx.Ctx, svc)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
svcCtx.IsActive = true
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *Processor) Delete(event watch.Event) (bool, error) {
|
||||
svc, ok := event.Object.(*v1.Service)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("(svcs) unable to parse Kubernetes services from API watcher")
|
||||
}
|
||||
svcCtx, err := p.getServiceContext(svc.UID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("(svcs) unable to get context: %w", err)
|
||||
}
|
||||
|
||||
if svcCtx != nil {
|
||||
// We only care about LoadBalancer services
|
||||
if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// We can ignore this service
|
||||
if svc.Annotations[kubevip.LoadbalancerIgnore] == "true" {
|
||||
log.Info("(svcs) ignore annotation for kube-vip", "service name", svc.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If no leader election is enabled, delete routes here
|
||||
if !p.config.EnableLeaderElection && !p.config.EnableServicesElection &&
|
||||
p.config.EnableRoutingTable && svcCtx.HasConfiguredNetworks() {
|
||||
if errs := endpoints.ClearRoutes(svc, &p.ServiceInstances); len(errs) == 0 {
|
||||
svcCtx.ConfiguredNetworks.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an active service then and additional leaderElection will handle stopping
|
||||
err = p.deleteService(svc.UID)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
|
||||
// Calls the cancel function of the context
|
||||
log.Warn("(svcs) The load balancer was deleted, cancelling context")
|
||||
svcCtx.IsActive = false
|
||||
svcCtx.Cancel()
|
||||
log.Warn("(svcs) waiting for load balancer to finish")
|
||||
<-svcCtx.Ctx.Done()
|
||||
p.svcMap.Delete(svc.UID)
|
||||
}
|
||||
|
||||
log.Info("(svcs) deleted", "service name", svc.Name, "namespace", svc.Namespace)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *Processor) Stop() {
|
||||
for _, instance := range p.ServiceInstances {
|
||||
for _, cluster := range instance.Clusters {
|
||||
cluster.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) getServiceContext(uid types.UID) (*servicecontext.Context, error) {
|
||||
svcCtx, ok := p.svcMap.Load(uid)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, ok := svcCtx.(*servicecontext.Context)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to cast service context pointer - UID: %s", uid)
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (p *Processor) CountRouteReferences(route *netlink.Route) int {
|
||||
return endpoints.CountRouteReferences(route, &p.ServiceInstances)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package manager
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,13 +12,19 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/vishvananda/netlink"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/cluster"
|
||||
"github.com/kube-vip/kube-vip/pkg/egress"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/instance"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/upnp"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
)
|
||||
|
||||
@@ -30,58 +36,70 @@ const (
|
||||
ActionNone ServiceInstanceAction = "none"
|
||||
)
|
||||
|
||||
func (sm *Manager) syncServices(ctx context.Context, svc *v1.Service) error {
|
||||
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 := sm.getServiceInstanceAction(svc)
|
||||
action := p.getServiceInstanceAction(svc)
|
||||
switch action {
|
||||
case ActionDelete:
|
||||
log.Debug("[service] delete", "namespace", svc.Namespace, "name", svc.Name)
|
||||
if err := sm.deleteService(svc.UID); err != nil {
|
||||
// remove the label from the node before deleting the service
|
||||
if err := p.nodeLabelManager.RemoveLabel(ctx, svc); err != nil {
|
||||
return fmt.Errorf("error removing label from node: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("[service] delete", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
if err := p.deleteService(svc.UID); err != nil {
|
||||
return fmt.Errorf("error deleting service %s/%s: %w", svc.Namespace, svc.Name, err)
|
||||
}
|
||||
case ActionAdd:
|
||||
log.Debug("[service] add", "namespace", svc.Namespace, "name", svc.Name)
|
||||
if err := sm.addService(ctx, svc); err != nil {
|
||||
log.Debug("[service] add", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
if err := p.addService(ctx, svc); err != nil {
|
||||
return fmt.Errorf("error adding service %s/%s: %w", svc.Namespace, svc.Name, err)
|
||||
}
|
||||
|
||||
// add the label to the node after adding the service
|
||||
if err := p.nodeLabelManager.AddLabel(ctx, svc); err != nil {
|
||||
return fmt.Errorf("error adding label to node: %w", err)
|
||||
}
|
||||
case ActionNone:
|
||||
log.Debug("[service] no action", "namespace", svc.Namespace, "name", svc.Name)
|
||||
log.Debug("[service] no action", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
}
|
||||
log.Debug("[FINISHED] Service Sync", "namespace", svc.Namespace, "name", svc.Name)
|
||||
log.Debug("[FINISHED] Service Sync", "namespace", svc.Namespace, "name", svc.Name, "uid", svc.UID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) getServiceInstanceAction(svc *v1.Service) ServiceInstanceAction {
|
||||
func (p *Processor) getServiceInstanceAction(svc *v1.Service) ServiceInstanceAction {
|
||||
// protect against multiple calls
|
||||
addresses := cluster.FetchServiceAddresses(svc)
|
||||
ingressIPs := cluster.FetchLoadBalancerIngressAddresses(svc)
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
// get the annotations or legacy values from manual configuration
|
||||
addresses, hostnames := instance.FetchServiceAddresses(svc)
|
||||
// get the status information of the LB Service
|
||||
statusAddresses, _ := instance.FetchLoadBalancerIngress(svc)
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
for _, instance := range sm.serviceInstances {
|
||||
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" {
|
||||
if address != "0.0.0.0" && address != "::" {
|
||||
return ActionDelete
|
||||
}
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(ingressIPs, instance.DHCPInterfaceIP) {
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, instance.DHCPInterfaceIP) {
|
||||
return ActionDelete
|
||||
}
|
||||
}
|
||||
if !instance.IsDHCP {
|
||||
if address == "0.0.0.0" {
|
||||
if address == "0.0.0.0" || address == "::" {
|
||||
return ActionDelete
|
||||
}
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(ingressIPs, address) {
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !slices.Contains(statusAddresses, address) {
|
||||
return ActionDelete
|
||||
}
|
||||
}
|
||||
if !comparePortsAndPortStatuses(svc) {
|
||||
if len(svc.Status.LoadBalancer.Ingress) > 0 && !comparePortsAndPortStatuses(svc) {
|
||||
return ActionDelete
|
||||
}
|
||||
}
|
||||
@@ -89,8 +107,8 @@ func (sm *Manager) getServiceInstanceAction(svc *v1.Service) ServiceInstanceActi
|
||||
return ActionNone
|
||||
}
|
||||
}
|
||||
if len(addresses) > 0 {
|
||||
log.Debug("No matching service instance found", "service", svc.Name, "namespace", svc.Namespace, "addresses", addresses)
|
||||
if len(addresses) > 0 || len(hostnames) > 0 {
|
||||
log.Debug("no matching service instance found", "service", svc.Name, "namespace", svc.Namespace, "addresses", addresses, "hostnames", hostnames)
|
||||
return ActionAdd // If no matching instance is found, we need to add a new service instance
|
||||
}
|
||||
return ActionNone
|
||||
@@ -112,24 +130,24 @@ func comparePortsAndPortStatuses(svc *v1.Service) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (sm *Manager) addService(ctx context.Context, svc *v1.Service) error {
|
||||
func (p *Processor) addService(ctx context.Context, svc *v1.Service) error {
|
||||
// protect against addService while reading
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
newService, err := cluster.NewInstance(svc, sm.config, sm.intfMgr, sm.arpMgr)
|
||||
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(newService.VIPConfigs[x], sm.bgpServer, svc.Name, &sm.serviceInstances)
|
||||
newService.Clusters[x].StartLoadBalancerService(ctx, newService.VIPConfigs[x], p.bgpServer, svc.Name, p.CountRouteReferences)
|
||||
}
|
||||
|
||||
sm.upnpMap(ctx, newService)
|
||||
p.upnpMap(ctx, newService)
|
||||
|
||||
if newService.IsDHCP && len(newService.VIPConfigs) == 1 {
|
||||
go func() {
|
||||
@@ -137,8 +155,8 @@ func (sm *Manager) addService(ctx context.Context, svc *v1.Service) error {
|
||||
log.Debug("IP changed", "ip", ip)
|
||||
newService.VIPConfigs[0].VIP = ip
|
||||
newService.DHCPInterfaceIP = ip
|
||||
if !sm.config.DisableServiceUpdates {
|
||||
if err := sm.updateStatus(newService); err != nil {
|
||||
if !p.config.DisableServiceUpdates {
|
||||
if err := p.updateStatus(newService); err != nil {
|
||||
log.Warn("updating svc", "err", err)
|
||||
}
|
||||
}
|
||||
@@ -147,26 +165,26 @@ func (sm *Manager) addService(ctx context.Context, svc *v1.Service) error {
|
||||
}()
|
||||
}
|
||||
|
||||
sm.serviceInstances = append(sm.serviceInstances, newService)
|
||||
p.ServiceInstances = append(p.ServiceInstances, newService)
|
||||
|
||||
if !sm.config.DisableServiceUpdates {
|
||||
if !p.config.DisableServiceUpdates {
|
||||
log.Debug("[service] update", "namespace", newService.ServiceSnapshot.Namespace, "name", newService.ServiceSnapshot.Name)
|
||||
if err := sm.updateStatus(newService); err != nil {
|
||||
if err := p.updateStatus(newService); err != nil {
|
||||
log.Error("[service] updating status", "namespace", newService.ServiceSnapshot.Namespace, "name", newService.ServiceSnapshot.Name, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
serviceIPs := cluster.FetchServiceAddresses(svc)
|
||||
serviceIPs, _ := instance.FetchServiceAddresses(svc)
|
||||
// Check if we need to flush any conntrack connections (due to some dangling conntrack connections)
|
||||
if svc.Annotations[flushContrack] == "true" {
|
||||
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[egressDestinationPorts], svc.Annotations[egressSourcePorts])
|
||||
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[egressDestinationPorts], svc.Annotations[egressSourcePorts])
|
||||
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)
|
||||
}
|
||||
@@ -174,65 +192,65 @@ func (sm *Manager) addService(ctx context.Context, svc *v1.Service) error {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 sm.config.EgressWithNftables {
|
||||
if p.config.EgressWithNftables {
|
||||
// Ensure that kernel modules are loaded and report back missing modules.
|
||||
err = sm.nftablesCheck()
|
||||
err = p.nftablesCheck()
|
||||
if err != nil {
|
||||
log.Error("[service] configuring egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
log.Warn("[service] configuring nft egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
} else {
|
||||
// Ensure that kernel modules are loaded and report back missing modules.
|
||||
err = sm.iptablesCheck()
|
||||
err = p.iptablesCheck()
|
||||
if err != nil {
|
||||
log.Error("[service] configuring egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
log.Warn("[service] configuring egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
var podIP string
|
||||
errList := []error{}
|
||||
|
||||
// Should egress be IPv6
|
||||
if svc.Annotations[egressIPv6] == "true" {
|
||||
if svc.Annotations[kubevip.EgressIPv6] == "true" {
|
||||
// Does the service have an active IPv6 endpoint
|
||||
if svc.Annotations[activeEndpointIPv6] != "" {
|
||||
if svc.Annotations[kubevip.ActiveEndpointIPv6] != "" {
|
||||
for _, serviceIP := range serviceIPs {
|
||||
if sm.config.EnableEndpointSlices && vip.IsIPv6(serviceIP) {
|
||||
if !p.config.EnableEndpoints && utils.IsIPv6(serviceIP) {
|
||||
|
||||
podIP = svc.Annotations[activeEndpointIPv6]
|
||||
podIP = svc.Annotations[kubevip.ActiveEndpointIPv6]
|
||||
|
||||
err = sm.configureEgress(serviceIP, podIP, svc.Namespace, svc.Annotations)
|
||||
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)
|
||||
log.Warn("[service] configuring egress IPv6", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if svc.Annotations[activeEndpoint] != "" { // Not expected to be IPv6, so should be an IPv4 address
|
||||
} else if svc.Annotations[kubevip.ActiveEndpoint] != "" { // Not expected to be IPv6, so should be an IPv4 address
|
||||
for _, serviceIP := range serviceIPs {
|
||||
podIPs := svc.Annotations[activeEndpoint]
|
||||
if sm.config.EnableEndpointSlices && vip.IsIPv6(serviceIP) {
|
||||
podIPs = svc.Annotations[activeEndpointIPv6]
|
||||
podIPs := svc.Annotations[kubevip.ActiveEndpoint]
|
||||
if !p.config.EnableEndpoints && utils.IsIPv6(serviceIP) {
|
||||
podIPs = svc.Annotations[kubevip.ActiveEndpointIPv6]
|
||||
}
|
||||
err = sm.configureEgress(serviceIP, podIPs, svc.Namespace, svc.Annotations)
|
||||
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)
|
||||
log.Warn("[service] configuring egress IPv4", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(errList) == 0 {
|
||||
var provider epProvider
|
||||
if !sm.config.EnableEndpointSlices {
|
||||
provider = &endpointsProvider{label: "endpoints"}
|
||||
var provider providers.Provider
|
||||
if p.config.EnableEndpoints {
|
||||
provider = providers.NewEndpoints()
|
||||
} else {
|
||||
provider = &endpointslicesProvider{label: "endpointslices"}
|
||||
provider = providers.NewEndpointslices()
|
||||
}
|
||||
err = provider.updateServiceAnnotation(svc.Annotations[activeEndpoint], svc.Annotations[activeEndpointIPv6], svc, sm)
|
||||
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)
|
||||
log.Warn("[service] configuring egress", "service", svc.Name, "namespace", svc.Namespace, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,29 +261,31 @@ func (sm *Manager) addService(ctx context.Context, svc *v1.Service) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *Manager) deleteService(uid types.UID) error {
|
||||
func (p *Processor) deleteService(uid types.UID) error {
|
||||
// protect multiple calls
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
var updatedInstances []*cluster.Instance
|
||||
var serviceInstance *cluster.Instance
|
||||
var updatedInstances []*instance.Instance
|
||||
var serviceInstance *instance.Instance
|
||||
found := false
|
||||
for x := range sm.serviceInstances {
|
||||
log.Debug("service lookup", "target UID", uid, "found UID ", sm.serviceInstances[x].ServiceSnapshot.UID, "name", sm.serviceInstances[x].ServiceSnapshot.Name, "namespace", sm.serviceInstances[x].ServiceSnapshot.Namespace)
|
||||
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 sm.serviceInstances[x].ServiceSnapshot.UID != uid {
|
||||
updatedInstances = append(updatedInstances, sm.serviceInstances[x])
|
||||
if p.ServiceInstances[x].ServiceSnapshot.UID != uid {
|
||||
updatedInstances = append(updatedInstances, p.ServiceInstances[x])
|
||||
} else {
|
||||
// Flip the found when we match
|
||||
found = true
|
||||
serviceInstance = sm.serviceInstances[x]
|
||||
serviceInstance = p.ServiceInstances[x]
|
||||
}
|
||||
}
|
||||
|
||||
// If we've been through all services and not found the correct one then error
|
||||
if !found {
|
||||
// TODO: - fix UX
|
||||
// return fmt.Errorf("unable to find/stop service [%s]", uid)
|
||||
log.Warn("unable to find/stop service", "uid", uid)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -279,66 +299,75 @@ func (sm *Manager) deleteService(uid types.UID) error {
|
||||
shared := false
|
||||
vipSet := make(map[string]interface{})
|
||||
for x := range updatedInstances {
|
||||
for _, vip := range cluster.FetchServiceAddresses(updatedInstances[x].ServiceSnapshot) { //updatedInstances[x].ServiceSnapshot.Spec.LoadBalancerIP {
|
||||
vips, _ := instance.FetchServiceAddresses(updatedInstances[x].ServiceSnapshot)
|
||||
for _, vip := range vips { //updatedInstances[x].ServiceSnapshot.Spec.LoadBalancerIP {
|
||||
vipSet[vip] = nil
|
||||
}
|
||||
}
|
||||
for _, vip := range cluster.FetchServiceAddresses(serviceInstance.ServiceSnapshot) {
|
||||
vips, _ := instance.FetchServiceAddresses(serviceInstance.ServiceSnapshot)
|
||||
for _, vip := range vips {
|
||||
if _, found := vipSet[vip]; found {
|
||||
shared = true
|
||||
}
|
||||
}
|
||||
for x := range serviceInstance.Clusters {
|
||||
serviceInstance.Clusters[x].Stop()
|
||||
}
|
||||
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)
|
||||
return fmt.Errorf("[service] error finding VIP Interface: %v", err)
|
||||
}
|
||||
|
||||
err = netlink.LinkDel(macvlan)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting DHCP Link : %v", err)
|
||||
return fmt.Errorf("[service] error deleting DHCP Link : %v", err)
|
||||
}
|
||||
}
|
||||
for i := range serviceInstance.VIPConfigs {
|
||||
if serviceInstance.VIPConfigs[i].EnableBGP {
|
||||
sm.clearBGPHostsByInstance(serviceInstance)
|
||||
|
||||
if p.config.EnableBGP {
|
||||
endpoints.ClearBGPHostsByInstance(serviceInstance, p.bgpServer)
|
||||
}
|
||||
if p.config.EnableRoutingTable && (p.config.EnableLeaderElection || p.config.EnableServicesElection) {
|
||||
if errs := endpoints.ClearRoutesByInstance(serviceInstance.ServiceSnapshot, serviceInstance, &p.ServiceInstances); len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
log.Error("unable to clear routes", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We will need to tear down the egress
|
||||
if serviceInstance.ServiceSnapshot.Annotations[egress] == "true" {
|
||||
if serviceInstance.ServiceSnapshot.Annotations[activeEndpoint] != "" {
|
||||
log.Info("egress re-write enabled", "service", serviceInstance.ServiceSnapshot.Name)
|
||||
err := sm.TeardownEgress(serviceInstance.ServiceSnapshot.Annotations[activeEndpoint], serviceInstance.ServiceSnapshot.Spec.LoadBalancerIP, serviceInstance.ServiceSnapshot.Namespace, serviceInstance.ServiceSnapshot.Annotations)
|
||||
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("egress teardown", "err", err)
|
||||
log.Error("[service] egress teardown", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the service array
|
||||
sm.serviceInstances = updatedInstances
|
||||
p.ServiceInstances = updatedInstances
|
||||
|
||||
log.Info("Removed instance from manager", "uid", uid, "remaining advertised services", len(sm.serviceInstances))
|
||||
log.Info("Removed instance from manager", "uid", uid, "name", serviceInstance.ServiceSnapshot.Name, "remaining advertised services", len(p.ServiceInstances))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set up UPNP forwards for a service
|
||||
// We first try to use the more modern Pinhole API introduced in UPNPv2 and fall back to UPNPv2 Port Forwarding if no forward was successful
|
||||
func (sm *Manager) upnpMap(ctx context.Context, s *cluster.Instance) {
|
||||
func (p *Processor) upnpMap(ctx context.Context, s *instance.Instance) {
|
||||
if !isUPNPEnabled(s.ServiceSnapshot) {
|
||||
// Skip services missing the annotation
|
||||
return
|
||||
}
|
||||
if !sm.upnp {
|
||||
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
|
||||
@@ -348,13 +377,15 @@ func (sm *Manager) upnpMap(ctx context.Context, s *cluster.Instance) {
|
||||
// Reset Gateway IPs to remove stale addresses
|
||||
s.UPNPGatewayIPs = make([]string, 0)
|
||||
|
||||
for _, vip := range cluster.FetchServiceAddresses(s.ServiceSnapshot) {
|
||||
vips, _ := instance.FetchServiceAddresses(s.ServiceSnapshot)
|
||||
for _, vip := range vips {
|
||||
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.ConnectionClient.GetServiceClient().Location)
|
||||
|
||||
forwardSucessful := false
|
||||
if gw.WANIPv6FirewallControlClient != nil {
|
||||
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name, "gateway", gw.WANIPv6FirewallControlClient.Location)
|
||||
|
||||
pinholeID, pinholeErr := gw.WANIPv6FirewallControlClient.AddPinholeCtx(ctx, "0.0.0.0", uint16(port.Port), vip, uint16(port.Port), upnp.MapProtocolToIANA(string(port.Protocol)), 3600) //nolint TODO
|
||||
if pinholeErr == nil {
|
||||
forwardSucessful = true
|
||||
@@ -366,9 +397,17 @@ func (sm *Manager) upnpMap(ctx context.Context, s *cluster.Instance) {
|
||||
}
|
||||
// Fallback to PortForward
|
||||
if !forwardSucessful {
|
||||
log.Info("[UPNP] Adding map", "vip", vip, "port", port.Port, "service", s.ServiceSnapshot.Name)
|
||||
|
||||
portMappingErr := gw.ConnectionClient.AddPortMapping("0.0.0.0", uint16(port.Port), strings.ToUpper(string(port.Protocol)), uint16(port.Port), vip, true, s.ServiceSnapshot.Name, 3600) //nolint TODO
|
||||
if portMappingErr == nil {
|
||||
log.Info("[UPNP] Service should be accessible externally", "port", port.Port)
|
||||
ip, err := gw.ConnectionClient.GetExternalIPAddress()
|
||||
if err != nil {
|
||||
// Log the error but continue on the off chance the mapping was successful
|
||||
log.Error("[UPNP] Unable to get external IP address from gateway", "port", port.Port, "err", err)
|
||||
} else {
|
||||
log.Info("[UPNP] Service should be accessible externally", "port", port.Port, "externalip", ip)
|
||||
}
|
||||
forwardSucessful = true
|
||||
} else {
|
||||
//TODO: Cleanup
|
||||
@@ -391,7 +430,7 @@ func (sm *Manager) upnpMap(ctx context.Context, s *cluster.Instance) {
|
||||
s.UPNPGatewayIPs = slices.Compact(s.UPNPGatewayIPs)
|
||||
}
|
||||
|
||||
func (sm *Manager) updateStatus(i *cluster.Instance) error {
|
||||
func (p *Processor) updateStatus(i *instance.Instance) error {
|
||||
// let's retry status update every 10ms for 30s
|
||||
retryConfig := wait.Backoff{
|
||||
Steps: 3000,
|
||||
@@ -403,7 +442,7 @@ func (sm *Manager) updateStatus(i *cluster.Instance) error {
|
||||
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 := sm.clientSet.CoreV1().Services(i.ServiceSnapshot.Namespace).Get(context.TODO(), i.ServiceSnapshot.Name, metav1.GetOptions{})
|
||||
currentService, err := p.clientSet.CoreV1().Services(i.ServiceSnapshot.Namespace).Get(context.TODO(), i.ServiceSnapshot.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -413,14 +452,14 @@ func (sm *Manager) updateStatus(i *cluster.Instance) error {
|
||||
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 {
|
||||
// If we're using ARP then we can only broadcast the VIP from one place, also useful for other software when running BGP, add an annotation to the service
|
||||
if p.config.EnableARP || p.config.EnableBGP {
|
||||
// Add the current host
|
||||
currentServiceCopy.Annotations[vipHost] = sm.config.NodeName
|
||||
currentServiceCopy.Annotations[kubevip.VipHost] = p.config.NodeName
|
||||
}
|
||||
if i.DHCPInterfaceHwaddr != "" || i.DHCPInterfaceIP != "" {
|
||||
currentServiceCopy.Annotations[cluster.HWAddrKey] = i.DHCPInterfaceHwaddr
|
||||
currentServiceCopy.Annotations[cluster.RequestedIP] = 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" {
|
||||
@@ -429,7 +468,7 @@ func (sm *Manager) updateStatus(i *cluster.Instance) error {
|
||||
}
|
||||
|
||||
if !cmp.Equal(currentService, currentServiceCopy) {
|
||||
currentService, err = sm.clientSet.CoreV1().Services(currentServiceCopy.Namespace).Update(context.TODO(), currentServiceCopy, metav1.UpdateOptions{})
|
||||
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
|
||||
@@ -447,8 +486,8 @@ func (sm *Manager) updateStatus(i *cluster.Instance) error {
|
||||
ingresses := []v1.LoadBalancerIngress{}
|
||||
|
||||
for _, c := range i.VIPConfigs {
|
||||
if !vip.IsIP(c.VIP) {
|
||||
ips, err := vip.LookupHost(c.VIP, sm.config.DNSMode)
|
||||
if !utils.IsIP(c.VIP) {
|
||||
ips, err := utils.LookupHost(c.VIP, p.config.DNSMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -478,8 +517,8 @@ func (sm *Manager) updateStatus(i *cluster.Instance) error {
|
||||
}
|
||||
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 {
|
||||
_, err = p.clientSet.CoreV1().Services(currentService.Namespace).UpdateStatus(context.TODO(), currentService, metav1.UpdateOptions{})
|
||||
if err != nil && !apierrors.IsInvalid(err) {
|
||||
log.Error("updating Service", "namespace", i.ServiceSnapshot.Namespace, "name", i.ServiceSnapshot.Name, "err", err)
|
||||
return err
|
||||
}
|
||||
@@ -491,5 +530,21 @@ func (sm *Manager) updateStatus(i *cluster.Instance) error {
|
||||
}
|
||||
|
||||
func isUPNPEnabled(s *v1.Service) bool {
|
||||
return metav1.HasAnnotation(s.ObjectMeta, upnpEnabled) && s.Annotations[upnpEnabled] == "true"
|
||||
return metav1.HasAnnotation(s.ObjectMeta, kubevip.UpnpEnabled) && s.Annotations[kubevip.UpnpEnabled] == "true"
|
||||
}
|
||||
|
||||
// Refresh UPNP Port Forwards for all Service Instances registered in the processor
|
||||
func (p *Processor) RefreshUPNPForwards() {
|
||||
log.Info("Starting UPNP Port Refresher")
|
||||
for {
|
||||
time.Sleep(300 * time.Second)
|
||||
|
||||
log.Info("[UPNP] Refreshing Instances", "number of instances", len(p.ServiceInstances))
|
||||
for i := range p.ServiceInstances {
|
||||
p.upnpMap(context.TODO(), p.ServiceInstances[i])
|
||||
if err := p.updateStatus(p.ServiceInstances[i]); err != nil {
|
||||
log.Warn("[UPNP] Error updating service", "ip", p.ServiceInstances[i].ServiceSnapshot.Name, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
94
pkg/services/watch_endpoints.go
Normal file
94
pkg/services/watch_endpoints.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints"
|
||||
"github.com/kube-vip/kube-vip/pkg/endpoints/providers"
|
||||
"github.com/kube-vip/kube-vip/pkg/servicecontext"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
)
|
||||
|
||||
func (p *Processor) watchEndpoint(svcCtx *servicecontext.Context, id string, service *v1.Service, provider providers.Provider) error {
|
||||
log.Info("watching", "provider", provider.GetLabel(), "service_name", service.Name, "namespace", service.Namespace)
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
|
||||
leaderCtx, cancel := context.WithCancel(svcCtx.Ctx)
|
||||
defer cancel()
|
||||
|
||||
var leaderElectionActive bool
|
||||
|
||||
rw, err := provider.CreateRetryWatcher(leaderCtx, p.rwClientSet, service)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[%s] error watching endpoints: %w", provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
exitFunction := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-svcCtx.Ctx.Done():
|
||||
log.Debug("context cancelled", "provider", provider.GetLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
case <-p.shutdownChan:
|
||||
log.Debug("shutdown called", "provider", provider.GetLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
case <-exitFunction:
|
||||
log.Debug("function ending", "provider", provider.GetLabel())
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
// Cancel the context, which will in turn cancel the leadership
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ch := rw.ResultChan()
|
||||
|
||||
epProcessor := endpoints.NewEndpointProcessor(p.config, provider, p.bgpServer, &p.ServiceInstances)
|
||||
|
||||
var lastKnownGoodEndpoint string
|
||||
for event := range ch {
|
||||
// We need to inspect the event and get ResourceVersion out of it
|
||||
switch event.Type {
|
||||
|
||||
case watch.Added, watch.Modified:
|
||||
restart, err := epProcessor.AddOrModify(svcCtx, event, &lastKnownGoodEndpoint, service, id, &leaderElectionActive, p.StartServicesLeaderElection, &leaderCtx, &cancel)
|
||||
if restart {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("[%s] error while processing add/modify event: %w", provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
case watch.Deleted:
|
||||
if err := epProcessor.Delete(service, id); err != nil {
|
||||
return fmt.Errorf("[%s] error while processing delete event: %w", provider.GetLabel(), err)
|
||||
}
|
||||
|
||||
// Close the goroutine that will end the retry watcher, then exit the endpoint watcher function
|
||||
close(exitFunction)
|
||||
log.Info("stopping watching", "provider", provider.GetLabel(), "service name", service.Name, "namespace", service.Namespace)
|
||||
|
||||
return nil
|
||||
case watch.Error:
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, _ := errObject.(*apierrors.StatusError)
|
||||
log.Error("watch error", "provider", provider.GetLabel(), "err", statusErr)
|
||||
}
|
||||
}
|
||||
close(exitFunction)
|
||||
log.Info("stopping watching", "provider", provider.GetLabel(), "service name", service.Name, "namespace", service.Namespace)
|
||||
return nil //nolint:govet
|
||||
}
|
||||
183
pkg/services/watch_services.go
Normal file
183
pkg/services/watch_services.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/kube-vip/kube-vip/pkg/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/trafficmirror"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
// This function handles the watching of a services endpoints and updates a load balancers endpoint configurations accordingly
|
||||
func (p *Processor) ServicesWatcher(ctx context.Context, serviceFunc func(context.Context, *v1.Service) error) error {
|
||||
// first start port mirroring if enabled
|
||||
if err := p.startTrafficMirroringIfEnabled(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
// clean up traffic mirror related config
|
||||
err := p.stopTrafficMirroringIfEnabled()
|
||||
if err != nil {
|
||||
log.Error("Stopping traffic mirroring", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if p.config.ServiceNamespace == "" {
|
||||
// v1.NamespaceAll is actually "", but we'll stay with the const in case things change upstream
|
||||
p.config.ServiceNamespace = v1.NamespaceAll
|
||||
log.Info("(svcs) starting services watcher for all namespaces")
|
||||
} else {
|
||||
log.Info("(svcs) starting services watcher", "namespace", p.config.ServiceNamespace)
|
||||
}
|
||||
|
||||
// Use a restartable watcher, as this should help in the event of etcd or timeout issues
|
||||
rw, err := watchtools.NewRetryWatcherWithContext(ctx, "1", &cache.ListWatch{
|
||||
WatchFunc: func(_ metav1.ListOptions) (watch.Interface, error) {
|
||||
return p.rwClientSet.CoreV1().Services(p.config.ServiceNamespace).Watch(ctx, metav1.ListOptions{})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating services watcher: %s", err.Error())
|
||||
}
|
||||
exitFunction := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-p.shutdownChan:
|
||||
log.Debug("(svcs) shutdown called")
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
return
|
||||
case <-exitFunction:
|
||||
log.Debug("(svcs) function ending")
|
||||
// Stop the retry watcher
|
||||
rw.Stop()
|
||||
return
|
||||
}
|
||||
}()
|
||||
ch := rw.ResultChan()
|
||||
|
||||
// Used for tracking an active endpoint / pod
|
||||
for event := range ch {
|
||||
p.CountServiceWatchEvent.With(prometheus.Labels{"type": string(event.Type)}).Add(1)
|
||||
|
||||
// We need to inspect the event and get ResourceVersion out of it
|
||||
switch event.Type {
|
||||
case watch.Added, watch.Modified:
|
||||
restart, err := p.AddOrModify(ctx, event, serviceFunc)
|
||||
if restart {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("add/modify service error: %w", err)
|
||||
}
|
||||
case watch.Deleted:
|
||||
restart, err := p.Delete(event)
|
||||
if restart {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete service error: %w", err)
|
||||
}
|
||||
case watch.Bookmark:
|
||||
// Un-used
|
||||
case watch.Error:
|
||||
log.Error("Error attempting to watch Kubernetes services")
|
||||
|
||||
// This round trip allows us to handle unstructured status
|
||||
errObject := apierrors.FromObject(event.Object)
|
||||
statusErr, ok := errObject.(*apierrors.StatusError)
|
||||
if !ok {
|
||||
log.Error(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
|
||||
}
|
||||
|
||||
status := statusErr.ErrStatus
|
||||
log.Error("services", "err", status)
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(exitFunction)
|
||||
log.Warn("Stopping watching services for type: LoadBalancer in all namespaces")
|
||||
return nil
|
||||
}
|
||||
|
||||
func lbClassFilterLegacy(svc *v1.Service, config *kubevip.Config) bool {
|
||||
if svc == nil {
|
||||
log.Info("(svcs) service is nil, ignoring")
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass != nil {
|
||||
// if this isn't nil then it has been configured, check if it the kube-vip loadBalancer class
|
||||
if *svc.Spec.LoadBalancerClass != config.LoadBalancerClassName {
|
||||
log.Info("(svcs) specified the wrong loadBalancer class", "service name", svc.Name, "lbClass", *svc.Spec.LoadBalancerClass)
|
||||
return true
|
||||
}
|
||||
} else if config.LoadBalancerClassOnly {
|
||||
// if kube-vip is configured to only recognize services with kube-vip's lb class, then ignore the services without any lb class
|
||||
log.Info("(svcs) kube-vip configured to only recognize services with kube-vip's lb class but the service didn't specify any loadBalancer class, ignoring", "service name", svc.Name)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func lbClassFilter(svc *v1.Service, config *kubevip.Config) bool {
|
||||
if svc == nil {
|
||||
log.Info("(svcs) service is nil, ignoring")
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass == nil && config.LoadBalancerClassName != "" {
|
||||
log.Info("(svcs) no loadBalancer class, ignoring", "service name", svc.Name, "expected lbClass", config.LoadBalancerClassName)
|
||||
return true
|
||||
}
|
||||
if svc.Spec.LoadBalancerClass == nil && config.LoadBalancerClassName == "" {
|
||||
return false
|
||||
}
|
||||
if *svc.Spec.LoadBalancerClass != config.LoadBalancerClassName {
|
||||
log.Info("(svcs) specified wrong loadBalancer class, ignoring", "service name", svc.Name, "wrong lbClass", *svc.Spec.LoadBalancerClass, "expected lbClass", config.LoadBalancerClassName)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Processor) serviceInterface() string {
|
||||
svcIf := p.config.Interface
|
||||
if p.config.ServicesInterface != "" {
|
||||
svcIf = p.config.ServicesInterface
|
||||
}
|
||||
return svcIf
|
||||
}
|
||||
|
||||
func (p *Processor) startTrafficMirroringIfEnabled() error {
|
||||
if p.config.MirrorDestInterface != "" {
|
||||
svcIf := p.serviceInterface()
|
||||
log.Info("mirroring traffic", "src", svcIf, "dest", p.config.MirrorDestInterface)
|
||||
if err := trafficmirror.MirrorTrafficFromNIC(svcIf, p.config.MirrorDestInterface); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("skip starting traffic mirroring since it's not enabled.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) stopTrafficMirroringIfEnabled() error {
|
||||
if p.config.MirrorDestInterface != "" {
|
||||
svcIf := p.serviceInterface()
|
||||
log.Info("clean up qdisc config", "interface", svcIf)
|
||||
if err := trafficmirror.CleanupQDSICFromNIC(svcIf); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("skip stopping traffic mirroring since it's not enabled.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
72
pkg/utils/dns.go
Normal file
72
pkg/utils/dns.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
IPv4Family = "IPv4"
|
||||
IPv6Family = "IPv6"
|
||||
DualFamily = "dual"
|
||||
)
|
||||
|
||||
// LookupHost resolves dnsName and return an IP or an error
|
||||
func LookupHost(dnsName, dnsMode string) ([]string, error) {
|
||||
result, err := net.LookupHost(dnsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, errors.Errorf("empty address for %s", dnsName)
|
||||
}
|
||||
addrs := []string{}
|
||||
switch dnsMode {
|
||||
case strings.ToLower(IPv4Family), strings.ToLower(IPv6Family), DualFamily:
|
||||
a, err := getIPbyFamily(result, dnsMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs = append(addrs, a...)
|
||||
default:
|
||||
addrs = append(addrs, result[0])
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func getIPbyFamily(addresses []string, family string) ([]string, error) {
|
||||
var checkers []func(string) bool
|
||||
families := []string{}
|
||||
if family == DualFamily || family == strings.ToLower(IPv4Family) {
|
||||
checkers = append(checkers, IsIPv4)
|
||||
families = append(families, IPv4Family)
|
||||
}
|
||||
if family == DualFamily || family == strings.ToLower(IPv6Family) {
|
||||
checkers = append(checkers, IsIPv6)
|
||||
families = append(families, IPv6Family)
|
||||
}
|
||||
|
||||
addrs := []string{}
|
||||
for i, c := range checkers {
|
||||
addr, err := getIPbyChecker(addresses, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting %s address: %w", families[i], err)
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func getIPbyChecker(addresses []string, checker func(string) bool) (string, error) {
|
||||
for _, addr := range addresses {
|
||||
if checker(addr) {
|
||||
return addr, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("address not found")
|
||||
}
|
||||
58
pkg/utils/ip.go
Normal file
58
pkg/utils/ip.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// FormatIPWithSubnetMask takes a raw IP address and a subnet mask, and returns a formatted string in CIDR notation.
|
||||
func FormatIPWithSubnetMask(rawIP string, subnetMask string) (string, error) {
|
||||
|
||||
addr := fmt.Sprintf("%s/%s", rawIP, subnetMask)
|
||||
// Check if the input is valid
|
||||
_, _, err := net.ParseCIDR(addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid CIDR: %q, %w", addr, err)
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// IsIP returns if address is an IP or not
|
||||
func IsIP(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
// IsIPv4 returns true only if address is a valid IPv4 address
|
||||
func IsIPv4(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() != nil
|
||||
}
|
||||
|
||||
// IsIPv6 returns true only if address is a valid IPv6 address
|
||||
func IsIPv6(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
|
||||
func IsIPv4CIDR(cidr string) bool {
|
||||
ip, _, _ := net.ParseCIDR(cidr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() != nil
|
||||
}
|
||||
|
||||
func IsIPv6CIDR(cidr string) bool {
|
||||
ip, _, _ := net.ParseCIDR(cidr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
55
pkg/utils/netlink.go
Normal file
55
pkg/utils/netlink.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -13,15 +11,3 @@ func FileExists(filename string) bool {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package vip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -18,24 +18,28 @@ import (
|
||||
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/kubevip"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/networkinterface"
|
||||
)
|
||||
|
||||
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"
|
||||
defaultValidLft = 60
|
||||
iptablesComment = "%s kube-vip load balancer IP"
|
||||
iptablesCommentMarkRule = "kube-vip load balancer IP set mark for masquerade"
|
||||
defaultMaskIPv6 = 128
|
||||
defaultMaskIPv4 = 32
|
||||
)
|
||||
|
||||
// Network is an interface that enable managing operations for a given IP
|
||||
type Network interface {
|
||||
AddIP(precheck bool) (bool, error)
|
||||
AddIP(precheck bool, skipDAD bool) (bool, error)
|
||||
AddRoute(precheck bool) error
|
||||
DeleteIP() (bool, error)
|
||||
DeleteRoute() error
|
||||
UpdateRoutes() (bool, error)
|
||||
IsSet() (bool, error)
|
||||
IP() string
|
||||
CIDR() string
|
||||
IPisLinkLocal() bool
|
||||
@@ -62,6 +66,7 @@ type network struct {
|
||||
link *networkinterface.Link
|
||||
ports []v1.ServicePort
|
||||
serviceName string
|
||||
enableSecurity bool
|
||||
ignoreSecurity bool
|
||||
|
||||
dnsName string
|
||||
@@ -81,7 +86,8 @@ type network struct {
|
||||
|
||||
// NewConfig will attempt to provide an interface to the kernel network configuration
|
||||
func NewConfig(address string, iface string, loGlobalScope bool, subnet string, isDDNS bool, tableID int, tableType int,
|
||||
routingProtocol int, dnsMode, forwardMethod, iptablesBackend string, ipvsEnabled bool, intfMgr *networkinterface.Manager) ([]Network, error) {
|
||||
routingProtocol int, dnsMode, forwardMethod, iptablesBackend string,
|
||||
ipvsEnabled, enableSecurity bool, intfMgr *networkinterface.Manager) ([]Network, error) {
|
||||
networks := []Network{}
|
||||
|
||||
link, err := netlink.LinkByName(iface)
|
||||
@@ -91,7 +97,7 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
|
||||
|
||||
networkLink := intfMgr.Get(link)
|
||||
|
||||
if IsIP(address) {
|
||||
if utils.IsIP(address) {
|
||||
result := &network{
|
||||
link: networkLink,
|
||||
routeTable: tableID,
|
||||
@@ -117,6 +123,12 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
|
||||
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
|
||||
@@ -125,7 +137,8 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
|
||||
networks = append(networks, result)
|
||||
} else {
|
||||
// try to resolve the address
|
||||
ips, err := LookupHost(address, dnsMode)
|
||||
log.Debug("looking up host", "address", address, "dnsMode", dnsMode)
|
||||
ips, err := utils.LookupHost(address, dnsMode)
|
||||
if err != nil {
|
||||
// return early for ddns if no IP is allocated for the domain
|
||||
// when leader starts, should do get IP from DHCP for the domain
|
||||
@@ -139,8 +152,20 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
|
||||
iptablesBackend: iptablesBackend,
|
||||
isDDNS: isDDNS,
|
||||
dnsName: address,
|
||||
ipvsEnabled: ipvsEnabled,
|
||||
enableSecurity: enableSecurity,
|
||||
address: &netlink.Addr{ // create placeholder for the address
|
||||
IPNet: &net.IPNet{}, // that will be added later in the process
|
||||
Peer: &net.IPNet{},
|
||||
},
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
networks = append(networks, result)
|
||||
return networks, nil
|
||||
}
|
||||
@@ -157,16 +182,28 @@ func NewConfig(address string, iface string, loGlobalScope bool, subnet string,
|
||||
iptablesBackend: iptablesBackend,
|
||||
isDDNS: isDDNS,
|
||||
dnsName: address,
|
||||
ipvsEnabled: ipvsEnabled,
|
||||
enableSecurity: enableSecurity,
|
||||
}
|
||||
|
||||
// we're able to resolve store this as the initial IP
|
||||
|
||||
if result.address, err = netlink.ParseAddr(fmt.Sprintf("%s/%s", ip, subnet)); err != nil {
|
||||
subnets := Split(subnet)
|
||||
|
||||
s := subnet
|
||||
if len(subnets) > 1 {
|
||||
s = selectSubnet(ip, subnets)
|
||||
}
|
||||
|
||||
if result.address, err = netlink.ParseAddr(fmt.Sprintf("%s/%s", ip, s)); 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)
|
||||
}
|
||||
|
||||
@@ -292,13 +329,15 @@ func (configurator *network) UpdateRoutes() (bool, error) {
|
||||
}
|
||||
|
||||
// AddIP - Add an IP address to the interface
|
||||
func (configurator *network) AddIP(precheck bool) (bool, error) {
|
||||
// precheck: if true, check if the IP already exists before adding
|
||||
// skipDAD: if true, set IFA_F_NODAD flag for IPv6 addresses to skip Duplicate Address Detection
|
||||
func (configurator *network) AddIP(precheck bool, skipDAD 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 {
|
||||
if exists, err = configurator.IsSet(); err != nil {
|
||||
return false, errors.Wrap(err, "could not check if address exists")
|
||||
}
|
||||
}
|
||||
@@ -307,6 +346,15 @@ func (configurator *network) AddIP(precheck bool) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// For IPv6 addresses, optionally set NODAD flag to skip Duplicate Address Detection (DAD)
|
||||
// This prevents DADFAILED loops when recovering from a previous DADFAILED state
|
||||
// The flag tells the kernel to skip DAD, which is safe when we're re-adding
|
||||
// an address that we know should be ours (e.g., after DADFAILED recovery)
|
||||
if skipDAD && utils.IsIPv6(configurator.address.IP.String()) {
|
||||
configurator.address.Flags |= unix.IFA_F_NODAD
|
||||
log.Debug("Setting IFA_F_NODAD flag for IPv6 address to skip DAD", "ip", configurator.address.IP.String())
|
||||
}
|
||||
|
||||
if err := netlink.AddrReplace(configurator.link.Intf, configurator.address); err != nil {
|
||||
return false, errors.Wrap(err, "could not add ip")
|
||||
}
|
||||
@@ -319,7 +367,7 @@ func (configurator *network) AddIP(precheck bool) (bool, error) {
|
||||
}
|
||||
|
||||
func (configurator *network) configureIPTables() error {
|
||||
if os.Getenv("enable_service_security") == "true" && !configurator.ignoreSecurity {
|
||||
if configurator.enableSecurity && !configurator.ignoreSecurity {
|
||||
if err := configurator.addIptablesRulesToLimitTrafficPorts(); err != nil {
|
||||
return errors.Wrap(err, "could not add iptables rules to limit traffic ports")
|
||||
}
|
||||
@@ -468,7 +516,7 @@ func (configurator *network) DeleteIP() (bool, error) {
|
||||
configurator.link.Lock.Lock()
|
||||
defer configurator.link.Lock.Unlock()
|
||||
|
||||
result, err := configurator.isSet()
|
||||
result, err := configurator.IsSet()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "ip check in DeleteIP failed")
|
||||
}
|
||||
@@ -479,9 +527,10 @@ func (configurator *network) DeleteIP() (bool, error) {
|
||||
}
|
||||
|
||||
if err = netlink.AddrDel(configurator.link.Intf, configurator.address); err != nil {
|
||||
return false, fmt.Errorf("could not delete IP %q from interface %q: %w", configurator.address.IPNet.String(), configurator.link.Intf.Attrs().Name, err)
|
||||
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 true, errors.Wrap(err, "could not remove iptables rules to limit traffic ports")
|
||||
}
|
||||
@@ -563,7 +612,7 @@ func (configurator *network) IsDADFAIL() bool {
|
||||
configurator.link.Lock.Lock()
|
||||
defer configurator.link.Lock.Unlock()
|
||||
|
||||
if configurator.address == nil || !IsIPv6(configurator.address.IP.String()) {
|
||||
if configurator.address == nil || !utils.IsIPv6(configurator.address.IP.String()) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -588,13 +637,17 @@ func addressHasDADFAILEDFlag(address netlink.Addr) bool {
|
||||
}
|
||||
|
||||
// isSet - Check to see if VIP is set
|
||||
func (configurator *network) isSet() (result bool, err error) {
|
||||
func (configurator *network) IsSet() (result bool, err error) {
|
||||
var addresses []netlink.Addr
|
||||
|
||||
if configurator.address == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if configurator.address.Mask == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
addresses, err = netlink.AddrList(configurator.link.Intf, 0)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "could not list addresses")
|
||||
@@ -633,7 +686,13 @@ func (configurator *network) SetIP(ip string) error {
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -646,7 +705,7 @@ func (configurator *network) SetServicePorts(service *v1.Service) {
|
||||
|
||||
configurator.ports = service.Spec.Ports
|
||||
configurator.serviceName = service.Namespace + "/" + service.Name
|
||||
configurator.ignoreSecurity = service.Annotations[ignoreServiceSecurityAnnotation] == "true"
|
||||
configurator.ignoreSecurity = service.Annotations[kubevip.ServiceSecurityIgnore] == "true"
|
||||
}
|
||||
|
||||
// IP - return the IP Address
|
||||
@@ -654,6 +713,10 @@ func (configurator *network) IP() string {
|
||||
configurator.mu.Lock()
|
||||
defer configurator.mu.Unlock()
|
||||
|
||||
if configurator.address == nil || configurator.address.IP == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return configurator.address.IP.String()
|
||||
}
|
||||
|
||||
@@ -661,6 +724,10 @@ func (configurator *network) CIDR() string {
|
||||
configurator.mu.Lock()
|
||||
defer configurator.mu.Unlock()
|
||||
|
||||
if configurator.address == nil || configurator.address.IPNet == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return configurator.address.IPNet.String()
|
||||
}
|
||||
|
||||
@@ -734,9 +801,14 @@ func GarbageCollect(adapter, address string, intfMgr *networkinterface.Manager)
|
||||
}
|
||||
|
||||
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)
|
||||
selectedMask := mask
|
||||
var err error
|
||||
|
||||
if configurator.IP() != "" {
|
||||
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)
|
||||
@@ -744,16 +816,19 @@ func (configurator *network) SetMask(mask string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
size := 32
|
||||
family := "IPv4"
|
||||
size := defaultMaskIPv4
|
||||
family := utils.IPv4Family
|
||||
|
||||
if IsIPv6(configurator.IP()) {
|
||||
size = 128
|
||||
family = "IPv6"
|
||||
}
|
||||
if configurator.IP() != "" {
|
||||
if utils.IsIPv6(configurator.IP()) {
|
||||
size = defaultMaskIPv6
|
||||
family = utils.IPv6Family
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
)
|
||||
|
||||
// IPUpdater is the interface to plug dns updaters
|
||||
@@ -33,23 +35,24 @@ func (d *ipUpdater) Run(ctx context.Context) {
|
||||
return
|
||||
default:
|
||||
mode := "ipv4"
|
||||
if IsIPv6(d.vip.IP()) {
|
||||
if utils.IsIPv6(d.vip.IP()) {
|
||||
mode = "ipv6"
|
||||
}
|
||||
|
||||
ip, err := LookupHost(d.vip.DNSName(), mode)
|
||||
ip, err := utils.LookupHost(d.vip.DNSName(), mode)
|
||||
if err != nil {
|
||||
log.Warn("cannot lookup", "name", d.vip.DNSName(), "err", err)
|
||||
// fallback to renewing the existing IP
|
||||
ip = []string{d.vip.IP()}
|
||||
}
|
||||
|
||||
log.Info("setting IP", "address", ip)
|
||||
log.Debug("setting IP", "address", ip)
|
||||
if err := d.vip.SetIP(ip[0]); err != nil {
|
||||
log.Error("setting IP", "address", ip, "err", err)
|
||||
}
|
||||
|
||||
if _, err := d.vip.AddIP(false); err != nil {
|
||||
// Normal VIP addition for DNS, use skipDAD=false for normal DAD process
|
||||
if _, err := d.vip.AddIP(false, false); err != nil {
|
||||
log.Error("error adding virtual IP", "err", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
log "log/slog"
|
||||
|
||||
iptables "github.com/kube-vip/kube-vip/pkg/iptables"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
|
||||
ct "github.com/florianl/go-conntrack"
|
||||
)
|
||||
@@ -36,9 +37,9 @@ type Egress struct {
|
||||
}
|
||||
|
||||
func CreateIptablesClient(nftables bool, namespace string, protocol iptables.Protocol) (*Egress, error) {
|
||||
proto := "IPv4"
|
||||
proto := utils.IPv4Family
|
||||
if protocol == iptables.ProtocolIPv6 {
|
||||
proto = "IPv6"
|
||||
proto = utils.IPv6Family
|
||||
}
|
||||
log.Info("[egress] Creating an iptables client", "nftables", nftables, "protocol", proto)
|
||||
e := new(Egress)
|
||||
|
||||
118
pkg/vip/util.go
118
pkg/vip/util.go
@@ -5,78 +5,17 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
log "log/slog"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// LookupHost resolves dnsName and return an IP or an error
|
||||
func LookupHost(dnsName, dnsMode string) ([]string, error) {
|
||||
result, err := net.LookupHost(dnsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, errors.Errorf("empty address for %s", dnsName)
|
||||
}
|
||||
addrs := []string{}
|
||||
switch dnsMode {
|
||||
case "ipv4", "ipv6", "dual":
|
||||
a, err := getIPbyFamily(result, dnsMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs = append(addrs, a...)
|
||||
default:
|
||||
addrs = append(addrs, result[0])
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func getIPbyFamily(addresses []string, family string) ([]string, error) {
|
||||
var checkers []func(string) bool
|
||||
families := []string{}
|
||||
if family == "dual" || family == "ipv4" {
|
||||
checkers = append(checkers, IsIPv4)
|
||||
families = append(families, "IPv4")
|
||||
}
|
||||
if family == "dual" || family == "ipv6" {
|
||||
checkers = append(checkers, IsIPv6)
|
||||
families = append(families, "IPv6")
|
||||
}
|
||||
|
||||
addrs := []string{}
|
||||
for i, c := range checkers {
|
||||
addr, err := getIPbyChecker(addresses, c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting %s address: %w", families[i], err)
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func getIPbyChecker(addresses []string, checker func(string) bool) (string, error) {
|
||||
for _, addr := range addresses {
|
||||
if checker(addr) {
|
||||
return addr, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("address not found")
|
||||
}
|
||||
|
||||
// IsIP returns if address is an IP or not
|
||||
func IsIP(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
// getHostName return the hostname from the fqdn
|
||||
func getHostName(dnsName string) string {
|
||||
if dnsName == "" {
|
||||
@@ -87,40 +26,6 @@ func getHostName(dnsName string) string {
|
||||
return fields[0]
|
||||
}
|
||||
|
||||
// IsIPv4 returns true only if address is a valid IPv4 address
|
||||
func IsIPv4(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() != nil
|
||||
}
|
||||
|
||||
// IsIPv6 returns true only if address is a valid IPv6 address
|
||||
func IsIPv6(address string) bool {
|
||||
ip := net.ParseIP(address)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
|
||||
func IsIPv4CIDR(cidr string) bool {
|
||||
ip, _, _ := net.ParseCIDR(cidr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() != nil
|
||||
}
|
||||
|
||||
func IsIPv6CIDR(cidr string) bool {
|
||||
ip, _, _ := net.ParseCIDR(cidr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
return ip.To4() == nil
|
||||
}
|
||||
|
||||
// GetDefaultGatewayInterface return default gateway interface link
|
||||
func GetDefaultGatewayInterface() (*net.Interface, error) {
|
||||
routes, err := netlink.RouteList(nil, syscall.AF_INET)
|
||||
@@ -239,3 +144,22 @@ func GetNonLinkLocalIP(iface *netlink.Link, family int) (string, error) {
|
||||
|
||||
return "", fmt.Errorf("failed to find non-local IP on interface: %s", (*iface).Attrs().Name)
|
||||
}
|
||||
|
||||
func selectSubnet(address string, subnets []string) string {
|
||||
subnet := ""
|
||||
if utils.IsIPv4(address) {
|
||||
if subnets[0] != "" {
|
||||
subnet = subnets[0]
|
||||
} else {
|
||||
subnet = strconv.Itoa(defaultMaskIPv4)
|
||||
}
|
||||
} else {
|
||||
if len(subnets) > 1 && subnets[1] != "" {
|
||||
subnet = subnets[1]
|
||||
} else {
|
||||
subnet = strconv.Itoa(defaultMaskIPv6)
|
||||
}
|
||||
}
|
||||
|
||||
return subnet
|
||||
}
|
||||
|
||||
13
test.sh
13
test.sh
@@ -1,13 +0,0 @@
|
||||
#!/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
|
||||
3
testing/e2e/bgp/config.toml.tmpl
Normal file
3
testing/e2e/bgp/config.toml.tmpl
Normal file
@@ -0,0 +1,3 @@
|
||||
[global.config]
|
||||
as = {{ .AS }}
|
||||
router-id = "1.1.1.1"
|
||||
1150
testing/e2e/e2e_bgp_test.go
Normal file
1150
testing/e2e/e2e_bgp_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ package e2e_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -22,11 +23,13 @@ import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"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() {
|
||||
var _ = Describe("kube-vip routing table mode", Ordered, func() {
|
||||
if Mode == ModeRT {
|
||||
var (
|
||||
logger log.Logger
|
||||
@@ -34,10 +37,16 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
k8sImagePath string
|
||||
configPath string
|
||||
kubeVIPRoutingTableManifestTemplate *template.Template
|
||||
tempDirPath string
|
||||
tempDirPathRoot string
|
||||
v129 bool
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
var err error
|
||||
tempDirPathRoot, err = os.MkdirTemp("", "kube-vip-test-rt")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
BeforeEach(func() {
|
||||
klog.SetOutput(GinkgoWriter)
|
||||
logger = e2e.TestLogger{}
|
||||
@@ -55,21 +64,26 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
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())
|
||||
AfterAll(func() {
|
||||
if os.Getenv("E2E_KEEP_LOGS") != "true" {
|
||||
Expect(os.RemoveAll(tempDirPathRoot)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
Describe("kube-vip IPv4 control-plane routing table mode functionality", Ordered, func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 3
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
cpVIP = e2e.GenerateVIP(e2e.IPv4Family, SOffset.Get())
|
||||
cpVIP = e2e.GenerateVIP(utils.IPv4Family, SOffset.Get())
|
||||
|
||||
networking := &kindconfigv1alpha4.Networking{
|
||||
IPFamily: kindconfigv1alpha4.IPv4Family,
|
||||
@@ -82,13 +96,22 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
ControlPlaneEnable: "true",
|
||||
SvcEnable: "false",
|
||||
SvcElectionEnable: "false",
|
||||
EnableNodeLabeling: "false",
|
||||
}
|
||||
|
||||
clusterName, _ = prepareCluster(tempDirPath, "rt-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
var err error
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ipv4", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, ConfigMtx, logger)
|
||||
})
|
||||
|
||||
It("setups IPv4 address and route on control-plane node", func() {
|
||||
@@ -103,7 +126,7 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
container = fmt.Sprintf("%s-control-plane", clusterName)
|
||||
}
|
||||
|
||||
checkIPAddress(cpVIP, container, true)
|
||||
Expect(checkIPAddress(cpVIP, container, true)).To(BeTrue())
|
||||
e2e.CheckRoutePresence(cpVIP, container, true)
|
||||
}
|
||||
})
|
||||
@@ -113,13 +136,15 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 3
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
|
||||
cpVIP = e2e.GenerateVIP(e2e.IPv6Family, SOffset.Get())
|
||||
cpVIP = e2e.GenerateVIP(utils.IPv6Family, SOffset.Get())
|
||||
|
||||
networking := &kindconfigv1alpha4.Networking{
|
||||
IPFamily: kindconfigv1alpha4.IPv6Family,
|
||||
@@ -132,13 +157,22 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
ControlPlaneEnable: "true",
|
||||
SvcEnable: "false",
|
||||
SvcElectionEnable: "false",
|
||||
EnableNodeLabeling: "false",
|
||||
}
|
||||
|
||||
clusterName, _ = prepareCluster(tempDirPath, "rt-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
var err error
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ipv6", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, ConfigMtx, logger)
|
||||
})
|
||||
|
||||
It("setups IPv6 address and route on control-plane node", func() {
|
||||
@@ -153,7 +187,7 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
container = fmt.Sprintf("%s-control-plane", clusterName)
|
||||
}
|
||||
|
||||
checkIPAddress(cpVIP, container, true)
|
||||
Expect(checkIPAddress(cpVIP, container, true)).To(BeTrue())
|
||||
e2e.CheckRoutePresence(cpVIP, container, true)
|
||||
}
|
||||
})
|
||||
@@ -163,6 +197,8 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 3
|
||||
)
|
||||
@@ -183,11 +219,31 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
SvcElectionEnable: "false",
|
||||
}
|
||||
|
||||
clusterName, _ = prepareCluster(tempDirPath, "rt-ds-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
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,
|
||||
}
|
||||
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ds-ipv4", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, addSAN, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, ConfigMtx, logger)
|
||||
})
|
||||
|
||||
It("setups DualStack addresses and routes on control-plane nodes", func() {
|
||||
@@ -205,7 +261,7 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
addresses := vip.Split(cpVIP)
|
||||
|
||||
for _, addr := range addresses {
|
||||
checkIPAddress(addr, container, true)
|
||||
Expect(checkIPAddress(addr, container, true)).To(BeTrue())
|
||||
e2e.CheckRoutePresence(addr, container, true)
|
||||
}
|
||||
}
|
||||
@@ -216,6 +272,8 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 3
|
||||
)
|
||||
@@ -238,11 +296,31 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
SvcElectionEnable: "false",
|
||||
}
|
||||
|
||||
clusterName, _ = prepareCluster(tempDirPath, "rt-ds-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
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,
|
||||
}
|
||||
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ds-ipv6", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, addSAN, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, ConfigMtx, logger)
|
||||
})
|
||||
|
||||
It("setups DualStack addresses and routes on control-plane nodes", func() {
|
||||
@@ -260,7 +338,7 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
addresses := vip.Split(cpVIP)
|
||||
|
||||
for _, addr := range addresses {
|
||||
checkIPAddress(addr, container, true)
|
||||
Expect(checkIPAddress(addr, container, true)).To(BeTrue())
|
||||
e2e.CheckRoutePresence(addr, container, true)
|
||||
}
|
||||
}
|
||||
@@ -275,12 +353,13 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 1
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
cpVIP = e2e.GenerateVIP(e2e.IPv4Family, SOffset.Get())
|
||||
cpVIP = e2e.GenerateVIP(utils.IPv4Family, SOffset.Get())
|
||||
|
||||
networking := &kindconfigv1alpha4.Networking{
|
||||
IPFamily: kindconfigv1alpha4.IPv4Family,
|
||||
@@ -301,17 +380,25 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol}
|
||||
|
||||
clusterName, client = prepareCluster(tempDirPath, "rt-svc-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-svc-ipv4", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, 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)
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv4Family, offset)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 1, false)
|
||||
},
|
||||
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),
|
||||
@@ -319,8 +406,9 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
|
||||
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)
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv4Family, offset)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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),
|
||||
@@ -335,12 +423,13 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 1
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
cpVIP = e2e.GenerateVIP(e2e.IPv6Family, SOffset.Get())
|
||||
cpVIP = e2e.GenerateVIP(utils.IPv6Family, SOffset.Get())
|
||||
|
||||
networking := &kindconfigv1alpha4.Networking{
|
||||
IPFamily: kindconfigv1alpha4.IPv6Family,
|
||||
@@ -361,17 +450,25 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv6Protocol}
|
||||
|
||||
clusterName, client = prepareCluster(tempDirPath, "rt-svc-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-svc-ipv6", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, 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)
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv6Family, offset)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 1, false)
|
||||
},
|
||||
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),
|
||||
@@ -379,8 +476,9 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
|
||||
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)
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv6Family, offset)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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),
|
||||
@@ -395,6 +493,7 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 1
|
||||
)
|
||||
@@ -422,17 +521,25 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
|
||||
|
||||
clusterName, client = prepareCluster(tempDirPath, "rt-ds-svc-ipv4", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ds-svc-ipv4", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, 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)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 1, false)
|
||||
},
|
||||
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),
|
||||
@@ -441,7 +548,8 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
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)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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),
|
||||
@@ -456,6 +564,7 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 1
|
||||
)
|
||||
@@ -485,17 +594,25 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
|
||||
|
||||
clusterName, client = prepareCluster(tempDirPath, "rt-ds-svc-ipv6", k8sImagePath, v129, kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber)
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ds-svc-ipv6", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
cleanupCluster(clusterName, tempDirPath, ConfigMtx, logger)
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, 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)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 1, false)
|
||||
},
|
||||
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),
|
||||
@@ -504,17 +621,338 @@ var _ = Describe("kube-vip routing table mode", func() {
|
||||
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)
|
||||
testServiceRT(svcName, lbAddress, "plndr-svcs-lock", "kube-system", clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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 IPv4 services routing table mode functionality with services election", Ordered, func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 1
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
cpVIP = e2e.GenerateVIP(utils.IPv4Family, SOffset.Get())
|
||||
|
||||
networking := &kindconfigv1alpha4.Networking{
|
||||
IPFamily: kindconfigv1alpha4.IPv4Family,
|
||||
}
|
||||
|
||||
manifestValues = &e2e.KubevipManifestValues{
|
||||
ControlPlaneVIP: cpVIP,
|
||||
ImagePath: imagePath,
|
||||
ConfigPath: configPath,
|
||||
ControlPlaneEnable: "false",
|
||||
SvcEnable: "true",
|
||||
SvcElectionEnable: "true",
|
||||
}
|
||||
|
||||
var err error
|
||||
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol}
|
||||
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-svc-ipv4", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, ConfigMtx, logger)
|
||||
})
|
||||
|
||||
DescribeTable("configures an IPv4 routes for services",
|
||||
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv4Family, offset)
|
||||
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 1, false)
|
||||
},
|
||||
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(utils.IPv4Family, offset)
|
||||
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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 if common lease is used",
|
||||
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv4Family, offset)
|
||||
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, true)
|
||||
},
|
||||
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
|
||||
)
|
||||
})
|
||||
|
||||
Describe("kube-vip IPv6 services routing table mode functionality with services election", Ordered, func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
nodesNumber = 1
|
||||
)
|
||||
|
||||
BeforeAll(func() {
|
||||
cpVIP = e2e.GenerateVIP(utils.IPv6Family, SOffset.Get())
|
||||
|
||||
networking := &kindconfigv1alpha4.Networking{
|
||||
IPFamily: kindconfigv1alpha4.IPv6Family,
|
||||
}
|
||||
|
||||
manifestValues = &e2e.KubevipManifestValues{
|
||||
ControlPlaneVIP: cpVIP,
|
||||
ImagePath: imagePath,
|
||||
ConfigPath: configPath,
|
||||
ControlPlaneEnable: "false",
|
||||
SvcEnable: "true",
|
||||
SvcElectionEnable: "true",
|
||||
}
|
||||
|
||||
var err error
|
||||
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv6Protocol}
|
||||
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-svc-ipv6", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, ConfigMtx, logger)
|
||||
})
|
||||
|
||||
DescribeTable("configures an IPv6 routes for services",
|
||||
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv6Family, offset)
|
||||
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 1, false)
|
||||
},
|
||||
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(utils.IPv6Family, offset)
|
||||
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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 when common lease is used",
|
||||
func(svcName string, offset uint, trafficPolicy corev1.ServiceExternalTrafficPolicy) {
|
||||
lbAddress := e2e.GenerateVIP(utils.IPv6Family, offset)
|
||||
testServiceRT(svcName, lbAddress, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, true)
|
||||
},
|
||||
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
|
||||
)
|
||||
})
|
||||
|
||||
Describe("kube-vip DualStack services routing table mode functionality - IPv4 primary with services election", Ordered, func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
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: "true",
|
||||
EnableEndpointslices: "true",
|
||||
}
|
||||
|
||||
var err error
|
||||
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
|
||||
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ds-svc-ipv4", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, 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, false)
|
||||
},
|
||||
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, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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 when common lease is used",
|
||||
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, 2, true)
|
||||
},
|
||||
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
|
||||
)
|
||||
})
|
||||
|
||||
Describe("kube-vip DualStack services routing table mode functionality - IPv6 primary with services election", Ordered, func() {
|
||||
var (
|
||||
cpVIP string
|
||||
clusterName string
|
||||
client kubernetes.Interface
|
||||
manifestValues *e2e.KubevipManifestValues
|
||||
svcElection bool
|
||||
ipFamily []corev1.IPFamily
|
||||
tempDirPath string
|
||||
|
||||
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: "true",
|
||||
EnableEndpointslices: "true",
|
||||
}
|
||||
|
||||
var err error
|
||||
svcElection, err = strconv.ParseBool(manifestValues.SvcElectionEnable)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
ipFamily = []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol}
|
||||
|
||||
tempDirPath, err = os.MkdirTemp(tempDirPathRoot, "kube-vip-test")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
clusterName, client, _ = prepareCluster(tempDirPath, "rt-ds-svc-ipv6", k8sImagePath, v129,
|
||||
kubeVIPRoutingTableManifestTemplate, logger, manifestValues, networking, nodesNumber, nil, 1)
|
||||
})
|
||||
|
||||
AfterAll(func() {
|
||||
By(fmt.Sprintf("saving logs to %q", tempDirPath))
|
||||
err := e2e.GetLogs(context.Background(), client, tempDirPath)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
cleanupCluster(clusterName, 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, false)
|
||||
},
|
||||
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, fmt.Sprintf("kubevip-%s", svcName), dsNamespace, clusterName,
|
||||
trafficPolicy, client, svcElection, ipFamily, 2, false)
|
||||
},
|
||||
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 when common lease is used",
|
||||
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, 2, true)
|
||||
},
|
||||
Entry("with external traffic policy - cluster", "test-svc-cluster", SOffset.Get(), corev1.ServiceExternalTrafficPolicyCluster),
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
func testServiceRT(svcName, lbAddress, leaseName, leaseNamespace, clusterName string, trafficPolicy corev1.ServiceExternalTrafficPolicy,
|
||||
client kubernetes.Interface, serviceElection bool, ipFamily []corev1.IPFamily, numberOfServices int) {
|
||||
client kubernetes.Interface, serviceElection bool, ipFamily []corev1.IPFamily, numberOfServices int, commonLease bool) {
|
||||
lbAddresses := vip.Split(lbAddress)
|
||||
|
||||
services := []string{}
|
||||
@@ -523,28 +961,56 @@ func testServiceRT(svcName, lbAddress, leaseName, leaseNamespace, clusterName st
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
svcLease := ""
|
||||
if commonLease {
|
||||
svcLease = leaseName
|
||||
}
|
||||
createTestService(svc, dsNamespace, dsName, lbAddress,
|
||||
client, corev1.IPFamilyPolicyPreferDualStack, ipFamily, trafficPolicy)
|
||||
client, corev1.IPFamilyPolicyPreferDualStack, ipFamily, trafficPolicy, svcLease, 1)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
var container string
|
||||
if serviceElection {
|
||||
container = e2e.GetLeaseHolder(leaseName, leaseNamespace, client)
|
||||
for _, svc := range services {
|
||||
lease := fmt.Sprintf("kubevip-%s", svc)
|
||||
if commonLease {
|
||||
lease = leaseName
|
||||
}
|
||||
By(withTimestamp(fmt.Sprintf("getting lease holder for lease '%s/%s'", leaseNamespace, lease)))
|
||||
container = e2e.GetLeaseHolder(lease, leaseNamespace, client)
|
||||
for _, addr := range lbAddresses {
|
||||
By(withTimestamp(fmt.Sprintf("checking route presence for address %q on container %q", addr, container)))
|
||||
e2e.CheckRoutePresence(addr, container, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
container = fmt.Sprintf("%s-control-plane", clusterName)
|
||||
}
|
||||
|
||||
for _, addr := range lbAddresses {
|
||||
e2e.CheckRoutePresence(addr, container, true)
|
||||
for _, addr := range lbAddresses {
|
||||
By(withTimestamp(fmt.Sprintf("checking route presence for address %q on container %q", addr, container)))
|
||||
e2e.CheckRoutePresence(addr, container, true)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range numberOfServices {
|
||||
By(withTimestamp(fmt.Sprintf("deleting service %s/%s\n", dsNamespace, services[i])))
|
||||
expected := i < numberOfServices-1
|
||||
err := client.CoreV1().Services(dsNamespace).Delete(context.TODO(), services[i], metav1.DeleteOptions{})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
time.Sleep(time.Second)
|
||||
|
||||
for _, addr := range lbAddresses {
|
||||
By(withTimestamp(fmt.Sprintf("checking route presence for address %q on container %q - expected: %t", addr, container, expected)))
|
||||
e2e.CheckRoutePresence(addr, container, expected)
|
||||
if commonLease {
|
||||
By(withTimestamp(fmt.Sprintf("getting lease holder for lease '%s/%s' - expected: %t", leaseNamespace, leaseName, expected)))
|
||||
e2e.CheckLeasePresence(leaseName, leaseNamespace, client, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type san struct {
|
||||
ip *net.IP
|
||||
ipnet *net.IPNet
|
||||
}
|
||||
|
||||
97
testing/e2e/e2e_sigusr1_test.go
Normal file
97
testing/e2e/e2e_sigusr1_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
//go:build e2e
|
||||
// +build e2e
|
||||
|
||||
package e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gbytes"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
)
|
||||
|
||||
var _ = Describe("SIGUSR1 Signal Handler", func() {
|
||||
var (
|
||||
session *gexec.Session
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
if session != nil {
|
||||
session.Terminate()
|
||||
Eventually(session).Should(gexec.Exit())
|
||||
}
|
||||
cancel()
|
||||
})
|
||||
|
||||
When("kube-vip receives SIGUSR1", func() {
|
||||
It("should dump configuration without terminating", func() {
|
||||
Skip("Skipping until signal handler is implemented")
|
||||
|
||||
kvPath := os.Getenv("KUBE_VIP_PATH")
|
||||
if kvPath == "" {
|
||||
kvPath = "../../kube-vip"
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, kvPath, "manager",
|
||||
"--address", "192.168.1.100",
|
||||
"--interface", "lo",
|
||||
"--port", "6443",
|
||||
)
|
||||
|
||||
var err error
|
||||
session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
err = session.Command.Process.Signal(syscall.SIGUSR1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Eventually(session.Out, "5s").Should(gbytes.Say("KUBE-VIP CONFIGURATION DUMP"))
|
||||
Eventually(session.Out).Should(gbytes.Say("VIP: 192.168.1.100"))
|
||||
|
||||
Consistently(session, "2s").ShouldNot(gexec.Exit())
|
||||
})
|
||||
|
||||
It("should handle multiple SIGUSR1 signals", func() {
|
||||
Skip("Skipping until signal handler is implemented")
|
||||
|
||||
kvPath := os.Getenv("KUBE_VIP_PATH")
|
||||
if kvPath == "" {
|
||||
kvPath = "../../kube-vip"
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, kvPath, "manager",
|
||||
"--address", "192.168.1.100",
|
||||
"--interface", "lo",
|
||||
)
|
||||
|
||||
var err error
|
||||
session, err = gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err = session.Command.Process.Signal(syscall.SIGUSR1)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
Eventually(session.Out).Should(gbytes.Say("KUBE-VIP CONFIGURATION DUMP"))
|
||||
Consistently(session, "2s").ShouldNot(gexec.Exit())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
ModeEnv = "TEST_MODE"
|
||||
ModeARP = "arp"
|
||||
ModeRT = "rt"
|
||||
ModeBGP = "bgp"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,7 +31,7 @@ func TestE2E(t *testing.T) {
|
||||
mode := os.Getenv(ModeEnv)
|
||||
if mode == "" {
|
||||
Mode = ModeARP
|
||||
} else if mode != ModeARP && mode != ModeRT {
|
||||
} else if mode != ModeARP && mode != ModeRT && mode != ModeBGP {
|
||||
log.Fatal("invalid", "mode", mode)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/onsi/gomega/format"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
"github.com/kube-vip/kube-vip/testing/e2e"
|
||||
"github.com/kube-vip/kube-vip/testing/e2e/etcd"
|
||||
)
|
||||
@@ -60,7 +61,7 @@ var _ = Describe("kube-vip with etcd leader election", func() {
|
||||
|
||||
test.kubeVipImage = os.Getenv("E2E_IMAGE_PATH")
|
||||
|
||||
test.vip = e2e.GenerateVIP(e2e.IPv4Family, 5)
|
||||
test.vip = e2e.GenerateVIP(utils.IPv4Family, 5)
|
||||
test.logger.Printf("Selected VIP %s", test.vip)
|
||||
|
||||
test.currentDir, err = os.Getwd()
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kube-vip/kube-vip/pkg/vip"
|
||||
"github.com/kube-vip/kube-vip/pkg/utils"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gexec"
|
||||
@@ -28,8 +28,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
IPv4Family = "IPv4"
|
||||
IPv6Family = "IPv6"
|
||||
DualstackFamily = "DualStack"
|
||||
DualstackFamilyIPv6 = "DualStackIPv6"
|
||||
)
|
||||
|
||||
func EnsureKindNetwork() {
|
||||
@@ -69,31 +69,37 @@ func EnsureKindNetwork() {
|
||||
}
|
||||
|
||||
func GenerateVIP(family string, offset uint) string {
|
||||
if family == DualstackFamily || family == DualstackFamilyIPv6 {
|
||||
return fmt.Sprintf("%s,%s", GenerateVIP(utils.IPv4Family, offset), GenerateVIP(utils.IPv6Family, offset))
|
||||
}
|
||||
|
||||
cidrs := getKindNetworkSubnetCIDRs()
|
||||
|
||||
for _, cidr := range cidrs {
|
||||
ip, ipNet, parseErr := net.ParseCIDR(cidr)
|
||||
Expect(parseErr).NotTo(HaveOccurred())
|
||||
if cidr != "" {
|
||||
ip, ipNet, parseErr := net.ParseCIDR(cidr)
|
||||
Expect(parseErr).NotTo(HaveOccurred())
|
||||
|
||||
if ip.To4() == nil && family == IPv6Family {
|
||||
lowerMask := binary.BigEndian.Uint64(ipNet.Mask[8:])
|
||||
lowerStart := binary.BigEndian.Uint64(ipNet.IP[8:])
|
||||
lowerEnd := (lowerStart & lowerMask) | (^lowerMask)
|
||||
if ip.To4() == nil && family == utils.IPv6Family {
|
||||
lowerMask := binary.BigEndian.Uint64(ipNet.Mask[8:])
|
||||
lowerStart := binary.BigEndian.Uint64(ipNet.IP[8:])
|
||||
lowerEnd := (lowerStart & lowerMask) | (^lowerMask)
|
||||
|
||||
chosenVIP := make([]byte, 16)
|
||||
// Copy upper half into chosenVIP
|
||||
copy(chosenVIP, ipNet.IP[0:8])
|
||||
// Copy lower half into chosenVIP
|
||||
binary.BigEndian.PutUint64(chosenVIP[8:], lowerEnd-uint64(offset))
|
||||
return net.IP(chosenVIP).String()
|
||||
} else if ip.To4() != nil && family == IPv4Family {
|
||||
mask := binary.BigEndian.Uint32(ipNet.Mask)
|
||||
start := binary.BigEndian.Uint32(ipNet.IP)
|
||||
end := (start & mask) | (^mask)
|
||||
chosenVIP := make([]byte, 16)
|
||||
// Copy upper half into chosenVIP
|
||||
copy(chosenVIP, ipNet.IP[0:8])
|
||||
// Copy lower half into chosenVIP
|
||||
binary.BigEndian.PutUint64(chosenVIP[8:], lowerEnd-uint64(offset))
|
||||
return net.IP(chosenVIP).String()
|
||||
} else if ip.To4() != nil && family == utils.IPv4Family {
|
||||
mask := binary.BigEndian.Uint32(ipNet.Mask)
|
||||
start := binary.BigEndian.Uint32(ipNet.IP)
|
||||
end := (start & mask) | (^mask)
|
||||
|
||||
chosenVIP := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(chosenVIP, end-uint32(offset))
|
||||
return net.IP(chosenVIP).String()
|
||||
chosenVIP := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(chosenVIP, end-uint32(offset))
|
||||
return net.IP(chosenVIP).String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +108,7 @@ func GenerateVIP(family string, offset uint) string {
|
||||
}
|
||||
|
||||
func GenerateDualStackVIP(offset uint) string {
|
||||
return GenerateVIP(IPv4Family, offset) + "," + GenerateVIP(IPv6Family, offset)
|
||||
return GenerateVIP(utils.IPv4Family, offset) + "," + GenerateVIP(utils.IPv6Family, offset)
|
||||
}
|
||||
|
||||
func getKindNetworkSubnetCIDRs() []string {
|
||||
@@ -133,7 +139,7 @@ func getKindNetworkSubnetCIDRs() []string {
|
||||
|
||||
func CheckIPAddressPresence(ip string, container string, expected bool) bool {
|
||||
family := "-4"
|
||||
if vip.IsIPv6(ip) {
|
||||
if utils.IsIPv6(ip) {
|
||||
family = "-6"
|
||||
}
|
||||
|
||||
@@ -146,14 +152,18 @@ func CheckIPAddressPresence(ip string, container string, expected bool) bool {
|
||||
cmd.Stdout = cmdOut
|
||||
cmd.Stderr = cmdErr
|
||||
if err := cmd.Run(); err != nil {
|
||||
By(fmt.Sprintf("contianer %q interfaces error: %s", container, cmdErr.String()))
|
||||
return false
|
||||
}
|
||||
|
||||
By(fmt.Sprintf("contianer %q interfaces:\n %s", container, cmdOut.String()))
|
||||
|
||||
return strings.Contains(cmdOut.String(), ip) == expected
|
||||
}
|
||||
|
||||
func CheckIPAddressPresenceByLease(name, namespace, ip string, client kubernetes.Interface, expected bool) bool {
|
||||
container := GetLeaseHolder(name, namespace, client)
|
||||
By("Lease holder: " + container)
|
||||
if container == "" {
|
||||
return false
|
||||
}
|
||||
@@ -162,7 +172,7 @@ func CheckIPAddressPresenceByLease(name, namespace, ip string, client kubernetes
|
||||
|
||||
func CheckRoutePresence(ip string, container string, expected bool) bool {
|
||||
family := "-4"
|
||||
if vip.IsIPv6(ip) {
|
||||
if utils.IsIPv6(ip) {
|
||||
family = "-6"
|
||||
}
|
||||
|
||||
@@ -179,23 +189,48 @@ func CheckRoutePresence(ip string, container string, expected bool) bool {
|
||||
cmd.Stderr = cmdErr
|
||||
cmd.Run()
|
||||
|
||||
By("Routes: " + cmdOut.String())
|
||||
|
||||
result = strings.Contains(cmdOut.String(), ip) == expected
|
||||
|
||||
return result
|
||||
}, "60s", "1s").Should(BeTrue())
|
||||
}, "120s", "1s").Should(BeTrue())
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func GetLeaseHolder(name, namespace string, client kubernetes.Interface) string {
|
||||
func CheckLeasePresence(name, namespace string, client kubernetes.Interface, expected bool) *coordinationv1.Lease {
|
||||
var lease *coordinationv1.Lease
|
||||
Eventually(func() error {
|
||||
var err error
|
||||
lease, err = client.CoordinationV1().Leases(namespace).Get(context.TODO(), name, metav1.GetOptions{})
|
||||
return err
|
||||
}, "300s").ShouldNot(HaveOccurred())
|
||||
}, "360s", "1s").ShouldNot(HaveOccurred())
|
||||
|
||||
Expect(lease).ToNot(BeNil())
|
||||
|
||||
if expected {
|
||||
Expect(lease.Spec.HolderIdentity).ToNot(BeNil())
|
||||
} else {
|
||||
if lease.Spec.HolderIdentity != nil {
|
||||
if expected {
|
||||
Expect(*lease.Spec.HolderIdentity).ToNot(BeEmpty())
|
||||
} else {
|
||||
Expect(*lease.Spec.HolderIdentity).To(BeEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lease
|
||||
}
|
||||
|
||||
func GetLeaseHolder(name, namespace string, client kubernetes.Interface) string {
|
||||
var lease *coordinationv1.Lease
|
||||
Eventually(func() string {
|
||||
lease = CheckLeasePresence(name, namespace, client, true)
|
||||
return *lease.Spec.HolderIdentity
|
||||
}, "360s", "1s").ShouldNot(BeEmpty())
|
||||
|
||||
return *lease.Spec.HolderIdentity
|
||||
}
|
||||
|
||||
|
||||
86
testing/e2e/kube-vip-bgp.yaml.tmpl
Normal file
86
testing/e2e/kube-vip-bgp.yaml.tmpl
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: kube-vip
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app: kube-vip
|
||||
spec:
|
||||
containers:
|
||||
- name: kube-vip
|
||||
args:
|
||||
- manager
|
||||
- --prometheusHTTPServer
|
||||
- ""
|
||||
env:
|
||||
- name: vip_loglevel
|
||||
value: "-4"
|
||||
- name: lb_class_legacy_handling
|
||||
value: "false"
|
||||
- name: lb_class_name
|
||||
- name: prometheus_server
|
||||
value: :2112
|
||||
- name: disable_service_updates
|
||||
value: "false"
|
||||
- name: vip_arp
|
||||
value: "false"
|
||||
- name: port
|
||||
value: "6443"
|
||||
- name: vip_cidr
|
||||
value: "32"
|
||||
- name: vip_leaderelection
|
||||
value: "false"
|
||||
- name: vip_interface
|
||||
value: lo
|
||||
- name: bgp_enable
|
||||
value: "true"
|
||||
- name: bgp_hold_time
|
||||
value: "3"
|
||||
- name: bgp_keepalive_interval
|
||||
value: "1"
|
||||
- name: bgp_sourceif
|
||||
value: "eth0"
|
||||
- name: bgp_routerid
|
||||
value: "2.2.2.2"
|
||||
- name: bgp_as
|
||||
value: "{{ .BGPAS }}"
|
||||
- name: bgp_peers
|
||||
value: "{{ .BGPPeers }}"
|
||||
- name: mpbgp_nexthop
|
||||
value: "{{ .MPBGPNexthop }}"
|
||||
- name: mpbgp_ipv4
|
||||
value: "{{ .MPBGPNexthopIPv4 }}"
|
||||
- name: mpbgp_ipv6
|
||||
value: "{{ .MPBGPNexthopIPv6 }}"
|
||||
- name: address
|
||||
value: "{{ .ControlPlaneVIP }}"
|
||||
- name: cp_enable
|
||||
value: "{{ .ControlPlaneEnable }}"
|
||||
- name: svc_enable
|
||||
value: "{{ .SvcEnable }}"
|
||||
- name: svc_election
|
||||
value: "{{ .SvcElectionEnable }}"
|
||||
- name: enable_node_labeling
|
||||
value: "{{ .EnableNodeLabeling }}"
|
||||
- name: enable_endpointslices
|
||||
value: "{{ .EnableEndpointslices }}"
|
||||
image: "{{ .ImagePath }}"
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
volumeMounts:
|
||||
- mountPath: /etc/kubernetes/admin.conf
|
||||
name: kubeconfig
|
||||
hostAliases:
|
||||
- hostnames:
|
||||
- kubernetes
|
||||
ip: 127.0.0.1
|
||||
hostNetwork: true
|
||||
volumes:
|
||||
- hostPath:
|
||||
path: "{{ .ConfigPath }}"
|
||||
name: kubeconfig
|
||||
@@ -4,6 +4,8 @@ kind: Pod
|
||||
metadata:
|
||||
name: kube-vip
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app: kube-vip
|
||||
spec:
|
||||
containers:
|
||||
- name: kube-vip
|
||||
@@ -28,6 +30,8 @@ spec:
|
||||
value: "1"
|
||||
- name: cp_enable
|
||||
value: "true"
|
||||
- name: enable_node_labeling
|
||||
value: "{{ .EnableNodeLabeling }}"
|
||||
image: "{{ .ImagePath }}"
|
||||
imagePullPolicy: Never
|
||||
securityContext:
|
||||
|
||||
@@ -4,6 +4,8 @@ kind: Pod
|
||||
metadata:
|
||||
name: kube-vip
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app: kube-vip
|
||||
spec:
|
||||
containers:
|
||||
- name: kube-vip
|
||||
@@ -44,6 +46,8 @@ spec:
|
||||
value: "{{ .SvcEnable }}"
|
||||
- name: svc_election
|
||||
value: "{{ .SvcElectionEnable }}"
|
||||
- name: enable_node_labeling
|
||||
value: "{{ .EnableNodeLabeling }}"
|
||||
- name: enable_endpointslices
|
||||
value: "{{ .EnableEndpointslices }}"
|
||||
image: "{{ .ImagePath }}"
|
||||
|
||||
@@ -4,6 +4,8 @@ kind: Pod
|
||||
metadata:
|
||||
name: kube-vip
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app: kube-vip
|
||||
spec:
|
||||
containers:
|
||||
- name: kube-vip
|
||||
@@ -38,6 +40,8 @@ spec:
|
||||
value: "{{ .SvcEnable }}"
|
||||
- name: svc_election
|
||||
value: "{{ .SvcElectionEnable }}"
|
||||
- name: enable_node_labeling
|
||||
value: "{{ .EnableNodeLabeling }}"
|
||||
- name: enable_endpointslices
|
||||
value: "{{ .EnableEndpointslices }}"
|
||||
image: "{{ .ImagePath }}"
|
||||
|
||||
129
testing/e2e/logs.go
Normal file
129
testing/e2e/logs.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
)
|
||||
|
||||
func GetLogs(ctx context.Context, client kubernetes.Interface, tempDirPath string) error {
|
||||
if os.Getenv("E2E_KEEP_LOGS") != "true" {
|
||||
if err := os.RemoveAll(tempDirPath); err != nil {
|
||||
return fmt.Errorf("failed to remove temporary directory %q: %w", tempDirPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
intCtx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
path := filepath.Join(tempDirPath, "pods.json")
|
||||
fpods, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %q: %w", path, err)
|
||||
}
|
||||
defer fpods.Close()
|
||||
|
||||
// list pods
|
||||
pods, err := client.CoreV1().Pods("").List(intCtx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get pods: %w", err)
|
||||
}
|
||||
data, err := json.Marshal(pods)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal pods data: %w", err)
|
||||
}
|
||||
if _, err := fpods.Write(data); err != nil {
|
||||
return fmt.Errorf("failed to save pods: %w", err)
|
||||
}
|
||||
|
||||
// list services
|
||||
path = filepath.Join(tempDirPath, "services.json")
|
||||
fsvcs, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %q: %w", path, err)
|
||||
}
|
||||
defer fpods.Close()
|
||||
|
||||
svcs, err := client.CoreV1().Services("").List(intCtx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get pods: %w", err)
|
||||
}
|
||||
data, err = json.Marshal(svcs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal pods data: %w", err)
|
||||
}
|
||||
if _, err := fsvcs.Write(data); err != nil {
|
||||
return fmt.Errorf("failed to save pods: %w", err)
|
||||
}
|
||||
|
||||
// list leases
|
||||
path = filepath.Join(tempDirPath, "leases.json")
|
||||
fleases, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %q: %w", path, err)
|
||||
}
|
||||
defer fpods.Close()
|
||||
|
||||
leases, err := client.CoordinationV1().Leases("").List(intCtx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get leases: %w", err)
|
||||
}
|
||||
data, err = json.Marshal(leases)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal leases data: %w", err)
|
||||
}
|
||||
if _, err := fleases.Write(data); err != nil {
|
||||
return fmt.Errorf("failed to save leases: %w", err)
|
||||
}
|
||||
|
||||
// get kube-vip pods
|
||||
listOptions := metav1.ListOptions{
|
||||
LabelSelector: "app=kube-vip",
|
||||
}
|
||||
|
||||
kvpods, err := client.CoreV1().Pods("").List(intCtx, listOptions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list pods: %w", err)
|
||||
}
|
||||
|
||||
for _, pod := range kvpods.Items {
|
||||
if pod.Status.Phase != corev1.PodRunning {
|
||||
continue
|
||||
}
|
||||
req := client.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &corev1.PodLogOptions{})
|
||||
podLogs, err := req.Stream(intCtx)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("reading request stream: %w", err)
|
||||
}
|
||||
defer podLogs.Close()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = io.Copy(buf, podLogs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy logs to buffer: %w", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(tempDirPath, fmt.Sprintf("%s-%s.log", pod.Namespace, pod.Name))
|
||||
err = os.WriteFile(path, buf.Bytes(), 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write the log file %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user