Reviewed-on: #1
Co-authored-by: Tobias Trabelsi <lerentis@uploadfilter24.eu>
Co-committed-by: Tobias Trabelsi <lerentis@uploadfilter24.eu>
This commit is contained in:
Tobias Trabelsi 2023-10-15 19:50:59 +00:00 committed by lerentis
parent b2bdedd1de
commit b72c1d1edb
20 changed files with 735 additions and 0 deletions

1
.gitignore vendored
View File

@ -21,3 +21,4 @@
# Go workspace file
go.work
myvalues.yaml

32
.woodpecker.yml Normal file
View File

@ -0,0 +1,32 @@
steps:
build:
image: woodpeckerci/plugin-docker-buildx
settings:
platforms: linux/arm64/v8
repo: lerentis/metallb-ip-floater
tags:
- latest
- ${CI_COMMIT_SHA}
password:
from_secret: docker_hub_password
username:
from_secret: docker_hub_username
when:
event:
- push
- pull_request
notify:
image: appleboy/drone-telegram
settings:
message: "Commit {{ commit.message }} ({{ commit.link }}) ran with build {{ build.number }} and finished with status {{ build.status }}."
to:
from_secret: telegram_userid
token:
from_secret: telegram_secret
when:
status:
- failure
- success
event:
- push
- pull_request

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM golang:1.21 as build
WORKDIR /app
COPY . .
RUN go mod tidy && CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags '-w -extldflags "-static"' -o metallb-ip-floater ./cmd/
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /etc/passwd /etc/passwd
COPY --from=build /etc/group /etc/group
COPY --from=build --chown=65534:65534 /app/metallb-ip-floater /usr/local/bin/metallb-ip-floater
USER nobody
ENTRYPOINT ["/usr/local/bin/metallb-ip-floater"]

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,24 @@
apiVersion: v2
name: metallb-ip-floater
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.0.1"

View File

@ -0,0 +1,2 @@
Service has been deployed
get logs: kubectl logs -f --namespace {{ .Release.Namespace }} $POD_NAME

View File

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "metallb-ip-floater.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "metallb-ip-floater.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "metallb-ip-floater.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "metallb-ip-floater.labels" -}}
helm.sh/chart: {{ include "metallb-ip-floater.chart" . }}
{{ include "metallb-ip-floater.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "metallb-ip-floater.selectorLabels" -}}
app.kubernetes.io/name: {{ include "metallb-ip-floater.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "metallb-ip-floater.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "metallb-ip-floater.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "metallb-ip-floater.fullname" . }}
labels:
{{- include "metallb-ip-floater.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "metallb-ip-floater.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "metallb-ip-floater.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "metallb-ip-floater.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- with .Values.env }}
{{- . | toYaml | trim | nindent 12 }}
{{- end }}
{{- if .Values.externalConfigSecret.enabled }}
envFrom:
- secretRef:
name: {{ .Values.externalConfigSecret.name }}
{{- end }}
ports:
- name: healthcheck
containerPort: 8080
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: healthcheck
readinessProbe:
httpGet:
path: /health
port: healthcheck
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "metallb-ip-floater.serviceAccountName" . }}
labels:
{{- include "metallb-ip-floater.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,70 @@
# Default values for metallb-ip-floater.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: lerentis/metallb-ip-floater
pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion.
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
env:
- name: METALLB_IP_FLOATER_LOGLEVEL
value: "Info"
- name: METALLB_IP_FLOATER_LABELSELECTOR
value: "kops.k8s.io/instance-role=Node"
- name: METALLB_IP_FLOATER_HCLOUD_TOKEN
value: "define_it"
- name: METALLB_IP_FLOATER_FLOATING_IP_NAME
value: "define_it"
externalConfigSecret:
enabled: false
name: ""
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}

86
cmd/main.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"fmt"
"time"
"git.uploadfilter24.eu/covidnetes/metallb-ip-floater/internal"
"git.uploadfilter24.eu/covidnetes/metallb-ip-floater/internal/utils"
log "github.com/sirupsen/logrus"
)
func main() {
cfg, err := internal.GenConfig()
utils.ConfigureLogger(cfg)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Error generating Config: %s", err.Error()))
}
go func() {
log.WithFields(log.Fields{
"Caller": "Main",
}).Info("Starting Health Endpoint")
internal.StartHealthEndpoint()
}()
log.WithFields(log.Fields{
"Caller": "Main",
}).Info("Entering main event loop")
for {
possibleCandidates, err := internal.GetSpeakableNodes(cfg)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Failed to get possible IPs: %s", err.Error()))
}
for _, ip := range possibleCandidates {
metrics, err := internal.GetMetrics(cfg, fmt.Sprintf("%s:%d/metrics", ip, 7472))
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Failed to get Metrics for IP %s: %s", ip, err.Error()))
}
parsedMetrics, err := internal.ParseMetrics(metrics)
for k, v := range parsedMetrics {
if k == "metallb_speaker_announced" {
log.WithFields(log.Fields{
"Caller": "Main",
}).Debug(fmt.Sprintf("Value of key metallb_speaker_announced on %s: %s", ip, v))
metrics_ip, err := internal.GetIpFromMetrics(v)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Failed to ge IP: %s", err.Error()))
}
log.WithFields(log.Fields{
"Caller": "Main",
}).Debug(fmt.Sprintf("Ip in metrics value on %s: %s", ip, metrics_ip))
speakerNodeID, err := internal.GetNodeID(cfg, ip)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Failed to get Node ID for IP %s: %s", ip, err.Error()))
}
if cfg.DryRun {
log.WithFields(log.Fields{
"Caller": "Main",
}).Info(fmt.Sprintf("Would Assign Floating IP to Node: %s", speakerNodeID.Name))
} else {
err = internal.AttachFloatingIpToNode(cfg, speakerNodeID)
if err != nil {
log.WithFields(log.Fields{
"Caller": "Main",
}).Fatal(fmt.Sprintf("Failed to Attach Floating IP to Node: %s", err.Error()))
}
}
}
}
}
time.Sleep(15 * time.Minute)
}
}

1
cmd/main_test.go Normal file
View File

@ -0,0 +1 @@
package main

3
config.json Normal file
View File

@ -0,0 +1,3 @@
{
"loglevel": "Debug"
}

28
go.mod Normal file
View File

@ -0,0 +1,28 @@
module git.uploadfilter24.eu/covidnetes/metallb-ip-floater
go 1.21.1
require (
github.com/gorilla/mux v1.8.0
github.com/hetznercloud/hcloud-go v1.51.0
github.com/jinzhu/configor v1.2.1
github.com/prometheus/client_model v0.5.0
github.com/prometheus/common v0.44.0
github.com/sirupsen/logrus v1.9.3
)
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
golang.org/x/net v0.13.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

69
go.sum Normal file
View File

@ -0,0 +1,69 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hetznercloud/hcloud-go v1.51.0 h1:Gsjh+GeSH1ZZwOhVBLDxqRFEJSctDu6Jva9YDnNYlk4=
github.com/hetznercloud/hcloud-go v1.51.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

34
internal/config.go Normal file
View File

@ -0,0 +1,34 @@
package internal
import (
"errors"
"fmt"
"time"
"github.com/jinzhu/configor"
)
type Config = struct {
LogLevel string `default:"Info" env:"METALLB_IP_FLOATER_LOGLEVEL"`
LabelSelector string `default:"kops.k8s.io/instance-role=Node" env:"METALLB_IP_FLOATER_LABELSELECTOR"`
Protocol string `default:"http" env:"METALLB_IP_FLOATER_PROTOCOL"`
HcloudToken string `default:"" env:"METALLB_IP_FLOATER_HCLOUD_TOKEN"`
FloatingIPName string `default:"" env:"METALLB_IP_FLOATER_FLOATING_IP_NAME"`
DryRun bool `default:"false" env:"METALLB_IP_FLOATER_DRY_RUN"`
}
func GenConfig() (cfg *Config, err error) {
cfg = &Config{}
err = configor.New(&configor.Config{
ENVPrefix: "METALLB_IP_FLOATER",
AutoReload: true,
Silent: true,
AutoReloadInterval: time.Minute}).Load(cfg, "config.json")
if err != nil {
return nil, errors.New(fmt.Sprintf("Error generating Config: %s", err.Error()))
}
return cfg, nil
}

31
internal/health.go Normal file
View File

@ -0,0 +1,31 @@
package internal
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func StartHealthEndpoint() {
r := mux.NewRouter()
r.Use(mux.CORSMethodMiddleware(r))
r.HandleFunc("/health", send200).Methods(http.MethodGet)
err := http.ListenAndServe("0.0.0.0:8080", r)
if err != nil {
log.WithFields(log.Fields{
"Caller": "StartHealthEndpoint",
}).Error(fmt.Sprintf("Error creating health endpoint: %s", err.Error()))
}
}
func send200(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte{})
if err != nil {
log.WithFields(log.Fields{
"Caller": "send200",
}).Error(fmt.Sprintf("Error answering health endpoint: %s", err.Error()))
}
}

88
internal/hetzner.go Normal file
View File

@ -0,0 +1,88 @@
package internal
import (
"context"
"errors"
"fmt"
"net"
"github.com/hetznercloud/hcloud-go/hcloud"
log "github.com/sirupsen/logrus"
)
func GetSpeakableNodes(cfg *Config) ([]string, error) {
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
server, _, err := client.Server.List(context.TODO(), hcloud.ServerListOpts{
ListOpts: hcloud.ListOpts{
LabelSelector: cfg.LabelSelector,
}})
if err != nil {
return nil, errors.New(fmt.Sprintf("Error listing Hetzner Nodes: %s", err.Error()))
}
ips := make([]string, len(server))
if server != nil {
for i, instance := range server {
log.WithFields(log.Fields{
"Caller": "GetSpeakableNodes",
}).Info(fmt.Sprintf("Found IP: %s", instance.PrivateNet[0].IP.String()))
ips[i] = instance.PrivateNet[0].IP.String()
}
} else {
return nil, errors.New(fmt.Sprintf("No Nodes found with label selector: %s", cfg.LabelSelector))
}
return ips, nil
}
func GetNodeID(cfg *Config, ip string) (hcloud.Server, error) {
// get can only be done via name or id: https://pkg.go.dev/github.com/hetznercloud/hcloud-go/v2/hcloud#ServerClient.Get
// we can iterate over all nodes and select the one that matches the IP we know from speaker: https://pkg.go.dev/github.com/hetznercloud/hcloud-go/v2/hcloud#ServerClient.List
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
serverList, _, err := client.Server.List(context.TODO(), hcloud.ServerListOpts{
ListOpts: hcloud.ListOpts{
Page: 1,
PerPage: 100,
},
Status: []hcloud.ServerStatus{hcloud.ServerStatusRunning},
})
if err != nil {
return hcloud.Server{}, errors.New(fmt.Sprintf("Could not get Server List from Hetzner: %s", err.Error()))
}
for _, server := range serverList {
log.WithFields(log.Fields{
"Caller": "GetNodeID",
}).Info(fmt.Sprintf("Checking Node %d for announcing IP", server.ID))
if server.PublicNet.IPv4.IP.Equal(net.ParseIP(ip)) {
log.WithFields(log.Fields{
"Caller": "GetNodeID",
}).Info(fmt.Sprintf("Match on Node %d", server.ID))
return *server, nil
}
for _, network := range server.PrivateNet {
if network.IP.Equal(net.ParseIP(ip)) {
log.WithFields(log.Fields{
"Caller": "GetNodeID",
}).Info(fmt.Sprintf("Match on Node %d", server.ID))
return *server, nil
}
}
}
return hcloud.Server{}, errors.New(fmt.Sprintf("Could not find correct server"))
}
func AttachFloatingIpToNode(cfg *Config, server hcloud.Server) error {
// get floating ip by name. name can be set to config https://github.com/hetznercloud/hcloud-go/blob/v2.3.0/hcloud/floating_ip.go#L117
client := hcloud.NewClient(hcloud.WithToken(cfg.HcloudToken))
floatingIP, _, err := client.FloatingIP.GetByName(context.TODO(), cfg.FloatingIPName)
if err != nil {
return errors.New(fmt.Sprintf("Could not find Floating IP by name: %s", err.Error()))
}
_, _, err = client.FloatingIP.Assign(context.TODO(), floatingIP, &server)
if err != nil {
log.WithFields(log.Fields{
"Caller": "AttachFloatingIpToNode",
}).Error(fmt.Sprintf("Error response while attaching floating ip: %s", err.Error()))
}
return err
}

55
internal/metrics.go Normal file
View File

@ -0,0 +1,55 @@
package internal
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
log "github.com/sirupsen/logrus"
)
func ParseMetrics(metrics string) (map[string]*dto.MetricFamily, error) {
reader := strings.NewReader(metrics)
var parser expfmt.TextParser
mf, err := parser.TextToMetricFamilies(reader)
if err != nil {
return nil, errors.New(fmt.Sprintf("Error decoding metrics: %s", err.Error()))
}
return mf, nil
}
func GetMetrics(cfg *Config, endpoint string) (string, error) {
resp, err := http.Get(fmt.Sprintf("%s://%s", cfg.Protocol, endpoint))
if err != nil {
return "", errors.New(fmt.Sprintf("Error getting metrics: %s", err.Error()))
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
bodyString := string(bodyBytes)
return bodyString, nil
}
return "", errors.New(fmt.Sprintf("None ok return code from metrics endoint: %s", resp.Status))
}
func GetIpFromMetrics(metrics *dto.MetricFamily) (string, error) {
for _, metric := range metrics.Metric {
for _, label := range metric.Label {
if *label.Name == "ip" {
log.WithFields(log.Fields{
"Caller": "Main",
}).Info(fmt.Sprintf("Found IP Label: %s", *label.Value))
return *label.Value, nil
}
}
}
return "", errors.New("Could not find 'ip' in metrics labels")
}

28
internal/utils/logging.go Normal file
View File

@ -0,0 +1,28 @@
package utils
import (
"os"
"git.uploadfilter24.eu/covidnetes/metallb-ip-floater/internal"
log "github.com/sirupsen/logrus"
)
func ConfigureLogger(cfg *internal.Config) {
switch cfg.LogLevel {
case "Debug":
log.SetLevel(log.DebugLevel)
case "Info":
log.SetLevel(log.InfoLevel)
case "Warn":
log.SetLevel(log.WarnLevel)
case "Error":
log.SetLevel(log.ErrorLevel)
default:
log.SetLevel(log.InfoLevel)
log.Warnf("Home: invalid log level supplied: '%s'", cfg.LogLevel)
}
log.SetFormatter(&log.JSONFormatter{})
log.SetOutput(os.Stdout)
}