Skip to content

Commit e7beeb3

Browse files
committed
feat(hcloud-fip-controller): add wrapper chart for HCloud Floating IP failover
Wraps cbeneke/hcloud-fip-controller (0.6.0, vendored from its OCI chart) to keep a Hetzner Cloud Floating IP attached to the active leader-elected node — so a hostNetwork workload needing a stable public IP (e.g. NetBird Coturn STUN/TURN, whose 49152-65535 UDP relay range can't sit behind a LoadBalancer) survives node failures. kubeaid defaults: NODE_ADDRESS_TYPE=external; floatingIPs and the HCLOUD_API_TOKEN secret are left for the per-cluster values overlay. The controller only reassigns the IP via the hcloud API — the node-interface binding (cloud-init/netplan) is the operator's responsibility (see README).
1 parent a6e47e6 commit e7beeb3

16 files changed

Lines changed: 499 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Patterns to ignore when building packages.
2+
.DS_Store
3+
.git/
4+
.gitignore
5+
*.tmproj
6+
.vscode/
7+
*.tgz
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dependencies:
2+
- name: hcloud-fip-controller
3+
repository: oci://registry-1.docker.io/cbeneke
4+
version: 0.6.0
5+
digest: sha256:c8a4f0e1bfd096c454269f28e86c95a003e3bc0de59f0f244880c44596012829
6+
generated: "2026-06-26T09:57:19.513596393+05:30"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: v2
2+
name: hcloud-fip-controller
3+
description: Wrapper chart for cbeneke/hcloud-fip-controller — keeps a Hetzner Cloud Floating IP attached to the active (leader-elected) node so a host-network workload such as NetBird Coturn (STUN/TURN) stays reachable at a stable public IP across node failures.
4+
type: application
5+
version: 1.0.0
6+
dependencies:
7+
- name: hcloud-fip-controller
8+
version: 0.6.0
9+
repository: oci://registry-1.docker.io/cbeneke
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# hcloud-fip-controller
2+
3+
Wrapper chart for [cbeneke/hcloud-fip-controller](https://github.com/cbeneke/hcloud-fip-controller).
4+
5+
It keeps a Hetzner Cloud **Floating IP** attached to the active (leader-elected)
6+
node, so a `hostNetwork` workload that must be reachable at a fixed public IP —
7+
e.g. NetBird **Coturn** (STUN/TURN, UDP 3478 / 5349 + the 49152–65535 relay
8+
range, which an LB can't front) — survives node failures: the leader pod assigns
9+
the Floating IP to its own node, and when that node dies a new leader reassigns
10+
it (lease-based, ~15s failover by default).
11+
12+
## Operator responsibilities
13+
14+
This chart deploys the controller only. Two things must be arranged outside it:
15+
16+
1. **`floatingIPs`** + **`existingSecretName`** — set per-cluster in the values
17+
overlay. `existingSecretName` must point to a Secret carrying an
18+
`HCLOUD_API_TOKEN` key (use a kubeaid SealedSecret; don't inline the token via
19+
`hcloudApiToken`).
20+
2. **Node networking** — the controller only moves the API *assignment*; it does
21+
**not** configure the node's interface. The Floating IP must already be bound
22+
on every candidate node's NIC via cloud-init / netplan, or the node won't
23+
answer for it once the IP lands there.
24+
25+
## Notes
26+
27+
- For real HA, spread the 3 replicas across nodes (podAntiAffinity / topology
28+
spread) so leader election can fail over when a node dies.
29+
- Land the workload it protects (e.g. Coturn) on a **dedicated worker**, not a
30+
control-plane node — a public 16k-port UDP relay does not belong next to
31+
etcd/apiserver.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Patterns to ignore when building packages.
2+
.DS_Store
3+
.git/
4+
.gitignore
5+
*.tmproj
6+
.idea/
7+
.vscode/
8+
*.bak
9+
*.tmp
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: v2
2+
appVersion: 0.6.0
3+
description: Controller to assign Hetzner Cloud Floating IPs to Kubernetes nodes
4+
home: https://github.com/cbeneke/hcloud-fip-controller
5+
keywords:
6+
- hetzner
7+
- hcloud
8+
- floating-ip
9+
- kubernetes
10+
maintainers:
11+
- name: cbeneke
12+
name: hcloud-fip-controller
13+
sources:
14+
- https://github.com/cbeneke/hcloud-fip-controller
15+
type: application
16+
version: 0.6.0
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{{ .Chart.Name }} {{ .Chart.AppVersion }} has been deployed as a {{ .Values.kind }}.
2+
3+
Release: {{ .Release.Name }}
4+
Namespace: {{ .Release.Namespace }}
5+
6+
API token secret: {{ include "hcloud-fip-controller.secretName" . }}
7+
{{ if .Values.floatingIPs -}}
8+
Managing {{ len .Values.floatingIPs }} floating IP(s) from the chart configuration.
9+
{{- else -}}
10+
Floating IPs are auto-discovered from the Hetzner Cloud API.
11+
{{- end }}
12+
13+
Check the controller status with:
14+
15+
kubectl -n {{ .Release.Namespace }} get pods -l app.kubernetes.io/instance={{ .Release.Name }}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{{/* Expand the name of the chart. */}}
2+
{{- define "hcloud-fip-controller.name" -}}
3+
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
4+
{{- end -}}
5+
6+
{{/* Create a default fully qualified app name. */}}
7+
{{- define "hcloud-fip-controller.fullname" -}}
8+
{{- if .Values.fullnameOverride -}}
9+
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
10+
{{- else -}}
11+
{{- $name := default .Chart.Name .Values.nameOverride -}}
12+
{{- if contains $name .Release.Name -}}
13+
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
14+
{{- else -}}
15+
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
16+
{{- end -}}
17+
{{- end -}}
18+
{{- end -}}
19+
20+
{{/* Chart name and version as used by the chart label. */}}
21+
{{- define "hcloud-fip-controller.chart" -}}
22+
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
23+
{{- end -}}
24+
25+
{{/* Common labels */}}
26+
{{- define "hcloud-fip-controller.labels" -}}
27+
helm.sh/chart: {{ include "hcloud-fip-controller.chart" . }}
28+
{{ include "hcloud-fip-controller.selectorLabels" . }}
29+
{{- if .Chart.AppVersion }}
30+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
31+
{{- end }}
32+
app.kubernetes.io/managed-by: {{ .Release.Service }}
33+
{{- end -}}
34+
35+
{{/* Selector labels */}}
36+
{{- define "hcloud-fip-controller.selectorLabels" -}}
37+
app.kubernetes.io/name: {{ include "hcloud-fip-controller.name" . }}
38+
app.kubernetes.io/instance: {{ .Release.Name }}
39+
{{- end -}}
40+
41+
{{/* Service account name */}}
42+
{{- define "hcloud-fip-controller.serviceAccountName" -}}
43+
{{- if .Values.serviceAccount.create -}}
44+
{{- default (include "hcloud-fip-controller.fullname" .) .Values.serviceAccount.name -}}
45+
{{- else -}}
46+
{{- default "default" .Values.serviceAccount.name -}}
47+
{{- end -}}
48+
{{- end -}}
49+
50+
{{/* Secret name holding the Hetzner Cloud API token */}}
51+
{{- define "hcloud-fip-controller.secretName" -}}
52+
{{- if .Values.existingSecretName -}}
53+
{{- .Values.existingSecretName -}}
54+
{{- else -}}
55+
{{- printf "%s-secrets" (include "hcloud-fip-controller.fullname" .) -}}
56+
{{- end -}}
57+
{{- end -}}
58+
59+
{{/* Image reference, defaulting the tag to "v<appVersion>" */}}
60+
{{- define "hcloud-fip-controller.image" -}}
61+
{{- $tag := .Values.image.tag | default (printf "v%s" .Chart.AppVersion) -}}
62+
{{- printf "%s:%s" .Values.image.repository $tag -}}
63+
{{- end -}}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{{- if .Values.floatingIPs }}
2+
apiVersion: v1
3+
kind: ConfigMap
4+
metadata:
5+
name: {{ include "hcloud-fip-controller.fullname" . }}-config
6+
namespace: {{ .Release.Namespace }}
7+
labels:
8+
{{- include "hcloud-fip-controller.labels" . | nindent 4 }}
9+
data:
10+
config.json: |
11+
{{- dict "hcloud_floating_ips" .Values.floatingIPs | toPrettyJson | nindent 4 }}
12+
{{- end }}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
{{- if and (not .Values.hcloudApiToken) (not .Values.existingSecretName) }}
2+
{{- fail "A Hetzner Cloud API token is required: set `hcloudApiToken` or point `existingSecretName` at a secret containing an HCLOUD_API_TOKEN key." }}
3+
{{- end }}
4+
{{- if and (not .Values.floatingIPs) (not .Values.floatingIPAutodiscovery) }}
5+
{{- fail "No floating IPs configured: set `floatingIPs` to the addresses to manage, or set `floatingIPAutodiscovery: true` to auto-discover them from the Hetzner Cloud API." }}
6+
{{- end }}
7+
apiVersion: apps/v1
8+
kind: {{ .Values.kind }}
9+
metadata:
10+
name: {{ include "hcloud-fip-controller.fullname" . }}
11+
namespace: {{ .Release.Namespace }}
12+
labels:
13+
{{- include "hcloud-fip-controller.labels" . | nindent 4 }}
14+
spec:
15+
selector:
16+
matchLabels:
17+
{{- include "hcloud-fip-controller.selectorLabels" . | nindent 6 }}
18+
{{- if eq .Values.kind "Deployment" }}
19+
replicas: {{ .Values.replicaCount }}
20+
strategy:
21+
{{- toYaml .Values.updateStrategy | nindent 4 }}
22+
{{- end }}
23+
template:
24+
metadata:
25+
labels:
26+
{{- include "hcloud-fip-controller.selectorLabels" . | nindent 8 }}
27+
{{- with .Values.podLabels }}
28+
{{- toYaml . | nindent 8 }}
29+
{{- end }}
30+
{{- with .Values.podAnnotations }}
31+
annotations:
32+
{{- toYaml . | nindent 8 }}
33+
{{- end }}
34+
spec:
35+
serviceAccountName: {{ include "hcloud-fip-controller.serviceAccountName" . }}
36+
{{- with .Values.imagePullSecrets }}
37+
imagePullSecrets:
38+
{{- toYaml . | nindent 8 }}
39+
{{- end }}
40+
containers:
41+
- name: {{ .Chart.Name }}
42+
image: {{ include "hcloud-fip-controller.image" . | quote }}
43+
imagePullPolicy: {{ .Values.image.pullPolicy }}
44+
ports:
45+
- name: health
46+
containerPort: {{ .Values.healthCheck.port }}
47+
{{- with .Values.livenessProbe }}
48+
livenessProbe:
49+
{{- toYaml . | nindent 12 }}
50+
{{- end }}
51+
{{- with .Values.readinessProbe }}
52+
readinessProbe:
53+
{{- toYaml . | nindent 12 }}
54+
{{- end }}
55+
env:
56+
- name: NODE_NAME
57+
valueFrom:
58+
fieldRef:
59+
fieldPath: spec.nodeName
60+
- name: POD_NAME
61+
valueFrom:
62+
fieldRef:
63+
fieldPath: metadata.name
64+
- name: NAMESPACE
65+
valueFrom:
66+
fieldRef:
67+
fieldPath: metadata.namespace
68+
- name: HEALTH_CHECK_ADDRESS
69+
value: ":{{ .Values.healthCheck.port }}"
70+
{{- with .Values.monitoring.otelEndpoint }}
71+
- name: OTEL_EXPORTER_OTLP_ENDPOINT
72+
value: {{ . | quote }}
73+
{{- end }}
74+
{{- range $key, $value := .Values.config }}
75+
- name: {{ $key }}
76+
value: {{ $value | quote }}
77+
{{- end }}
78+
envFrom:
79+
- secretRef:
80+
name: {{ include "hcloud-fip-controller.secretName" . }}
81+
{{- if .Values.floatingIPs }}
82+
volumeMounts:
83+
- name: config
84+
mountPath: /app/config
85+
readOnly: true
86+
{{- end }}
87+
{{- with .Values.resources }}
88+
resources:
89+
{{- toYaml . | nindent 12 }}
90+
{{- end }}
91+
{{- if .Values.floatingIPs }}
92+
volumes:
93+
- name: config
94+
configMap:
95+
name: {{ include "hcloud-fip-controller.fullname" . }}-config
96+
{{- end }}
97+
{{- with .Values.nodeSelector }}
98+
nodeSelector:
99+
{{- toYaml . | nindent 8 }}
100+
{{- end }}
101+
{{- with .Values.affinity }}
102+
affinity:
103+
{{- toYaml . | nindent 8 }}
104+
{{- end }}
105+
{{- with .Values.tolerations }}
106+
tolerations:
107+
{{- toYaml . | nindent 8 }}
108+
{{- end }}

0 commit comments

Comments
 (0)