Skip to content

Commit b9ba912

Browse files
pkieszczstubbi
andauthored
feat(chart): expose scheduling, command/args, and an optional PDB (#47)
Add nodeSelector, affinity, tolerations, topologySpreadConstraints, imagePullSecrets, priorityClassName, command and args to the Deployment, all empty by default so existing releases render unchanged. Add an optional PodDisruptionBudget (off by default; defaults to maxUnavailable:1 when enabled) for drain safety with replicas > 1: minAvailable and maxUnavailable are mutually exclusive positive integers, and minAvailable >= replicas (the un-evictable footgun) is rejected at render time. replicas is validated as a non-negative integer. Bump the chart to 2.1.0. Also correct the README image.tag default (latest -> "" / tracks appVersion), which had drifted from values.yaml. Co-authored-by: Jannes Stubbemann <jannes.stubbemann@gmail.com>
1 parent c9c8096 commit b9ba912

6 files changed

Lines changed: 141 additions & 6 deletions

File tree

charts/karpenter-provider-hetzner/Chart.yaml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: karpenter-provider-hetzner
33
description: Karpenter cloud provider for Hetzner Cloud
44
type: application
5-
version: 2.0.0
5+
version: 2.1.0
66
appVersion: "2.0.0"
77
# home points at Paperclip.inc (the project's canonical page) so listing
88
# "Homepage" links drive backlinks to the domain; GitHub stays as source.
@@ -31,7 +31,5 @@ annotations:
3131
artifacthub.io/category: integration-delivery
3232
artifacthub.io/containsSecurityUpdates: "false"
3333
artifacthub.io/changes: |
34-
- kind: changed
35-
description: "BREAKING: HCloudNodeClass graduated from karpenter.hetzner.cloud/v1alpha1 to /v1; re-apply node classes (no conversion webhook)"
3634
- kind: added
37-
description: "k3s agent bootstrap example and guide (examples/k3s-nodeclass.yaml, docs/k3s-bootstrap.md)"
35+
description: "Expose nodeSelector, affinity, tolerations, topologySpreadConstraints, imagePullSecrets, priorityClassName, command, args and an optional PodDisruptionBudget (minAvailable or maxUnavailable) on the Deployment via chart values"

charts/karpenter-provider-hetzner/README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,46 @@ Existing `v1alpha1` objects are not migrated automatically; recreate them under
3939
| `clusterName` | `""` (required) | Scopes which servers the controller manages |
4040
| `replicas` | `1` | Controller replicas |
4141
| `image.repository` | `ghcr.io/paperclipinc/karpenter-provider-hetzner` | Image |
42-
| `image.tag` | `latest` | Pin a released tag in production |
42+
| `image.tag` | `""` | Empty tracks the chart appVersion; pin a tag in production |
43+
| `image.pullPolicy` | `IfNotPresent` | Image pull policy |
4344
| `auth.secretRef.name` | `hcloud-token` | Secret holding the Hetzner token |
4445
| `auth.secretRef.key` | `token` | Key within the secret |
4546
| `serviceAccount.create` | `true` | Create the service account |
4647
| `serviceAccount.name` | `karpenter` | Service account name |
4748
| `metrics.port` | `8080` | Prometheus metrics port |
4849
| `healthProbe.port` | `8081` | Health/readiness probe port |
4950
| `resources` | see values.yaml | Container resources |
51+
| `nodeSelector` | `{}` | Pin the controller pod to specific nodes |
52+
| `affinity` | `{}` | `nodeAffinity` / `podAntiAffinity` |
53+
| `tolerations` | `[]` | Tolerate node taints |
54+
| `topologySpreadConstraints` | `[]` | Spread replicas across nodes/zones; needs a labelSelector matching the pod labels |
55+
| `imagePullSecrets` | `[]` | Pull from a private registry mirror |
56+
| `priorityClassName` | `""` | Pod priority class |
57+
| `podDisruptionBudget.enabled` | `false` | Create a PodDisruptionBudget (enable with `replicas > 1`) |
58+
| `podDisruptionBudget.minAvailable` | unset | Positive integer; mutually exclusive with `maxUnavailable` |
59+
| `podDisruptionBudget.maxUnavailable` | `1` (default) | Positive integer; never blocks drains (run `replicas >= 2` for availability) |
60+
| `command` | `[]` | Override the container entrypoint (advanced) |
61+
| `args` | `[]` | Controller flags, e.g. `--log-level`, `--feature-gates` |
5062
| `serviceMonitor.enabled` | `false` | Deploy a Service + ServiceMonitor for Prometheus Operator |
5163
| `serviceMonitor.interval` | `30s` | Scrape interval |
5264
| `serviceMonitor.additionalLabels` | `{}` | Extra labels on the ServiceMonitor (for Prometheus Operator selector) |
5365

5466
The CRD is installed from `crds/` automatically by Helm.
5567

68+
## Scheduling
69+
70+
`nodeSelector`, `affinity`, `tolerations`, `topologySpreadConstraints`,
71+
`imagePullSecrets` and `priorityClassName` pass straight through to the
72+
Deployment pod spec; `command` and `args` pass through to the container (the
73+
hardcoded probes expect `/healthz` and `/readyz`, so a custom command must serve
74+
them). All default to empty. Leader election is enabled by default, so `replicas > 1` won't
75+
double-reconcile — only the leader acts. For drain safety with `replicas > 1`,
76+
enable `podDisruptionBudget`: it defaults to `maxUnavailable: 1`, which never
77+
blocks drains (a single replica can still be evicted, so run `replicas >= 2` to
78+
keep the controller available). Configs that would block all voluntary drains
79+
(`minAvailable >= replicas` or `maxUnavailable: 0`) are rejected by design —
80+
checked at install/upgrade, so runtime scaling is the operator's responsibility.
81+
5682
## Prometheus Operator integration
5783

5884
When `serviceMonitor.enabled=true` the chart creates:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{{/*
2+
Validate that value is a decimal integer in a small range; fail the render with
3+
a clear message otherwise. The bound (0/1-999999) stays below 1e6 so Helm's
4+
float64/scientific-notation coercion (e.g. 1000000 -> "1e+06") can't slip a
5+
value past the regex. Set allowZero=true to permit 0 (replicas); otherwise >= 1.
6+
7+
Usage: {{ include "karpenter-hetzner.requireInt" (dict "value" .Values.replicas "name" "replicas" "allowZero" true) }}
8+
*/}}
9+
{{- define "karpenter-hetzner.requireInt" -}}
10+
{{- $re := "^[1-9][0-9]{0,5}$" -}}
11+
{{- $range := "1-999999" -}}
12+
{{- if .allowZero -}}
13+
{{- $re = "^(0|[1-9][0-9]{0,5})$" -}}
14+
{{- $range = "0-999999" -}}
15+
{{- end -}}
16+
{{- if not (regexMatch $re (printf "%v" .value)) -}}
17+
{{- fail (printf "%s must be an integer (%s), got %v" .name $range .value) -}}
18+
{{- end -}}
19+
{{- end -}}

charts/karpenter-provider-hetzner/templates/deployment.yaml

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{{- if not .Values.clusterName }}
22
{{- fail "clusterName is required: set --set clusterName=<your-cluster>" }}
33
{{- end }}
4+
{{- include "karpenter-hetzner.requireInt" (dict "value" .Values.replicas "name" "replicas" "allowZero" true) }}
45
apiVersion: apps/v1
56
kind: Deployment
67
metadata:
@@ -10,7 +11,7 @@ metadata:
1011
app.kubernetes.io/name: karpenter-provider-hetzner
1112
app.kubernetes.io/instance: {{ .Release.Name }}
1213
spec:
13-
replicas: {{ .Values.replicas }}
14+
replicas: {{ .Values.replicas | int }}
1415
selector:
1516
matchLabels:
1617
app.kubernetes.io/name: karpenter-provider-hetzner
@@ -21,13 +22,44 @@ spec:
2122
app.kubernetes.io/instance: {{ .Release.Name }}
2223
spec:
2324
serviceAccountName: {{ .Values.serviceAccount.name }}
25+
{{- with .Values.priorityClassName }}
26+
priorityClassName: {{ . | quote }}
27+
{{- end }}
28+
{{- with .Values.imagePullSecrets }}
29+
imagePullSecrets:
30+
{{- toYaml . | nindent 8 }}
31+
{{- end }}
32+
{{- with .Values.nodeSelector }}
33+
nodeSelector:
34+
{{- toYaml . | nindent 8 }}
35+
{{- end }}
36+
{{- with .Values.affinity }}
37+
affinity:
38+
{{- toYaml . | nindent 8 }}
39+
{{- end }}
40+
{{- with .Values.topologySpreadConstraints }}
41+
topologySpreadConstraints:
42+
{{- toYaml . | nindent 8 }}
43+
{{- end }}
44+
{{- with .Values.tolerations }}
45+
tolerations:
46+
{{- toYaml . | nindent 8 }}
47+
{{- end }}
2448
securityContext:
2549
seccompProfile:
2650
type: RuntimeDefault
2751
containers:
2852
- name: controller
2953
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
3054
imagePullPolicy: {{ .Values.image.pullPolicy }}
55+
{{- with .Values.command }}
56+
command:
57+
{{- toYaml . | nindent 12 }}
58+
{{- end }}
59+
{{- with .Values.args }}
60+
args:
61+
{{- toYaml . | nindent 12 }}
62+
{{- end }}
3163
env:
3264
- name: HCLOUD_TOKEN
3365
valueFrom:
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{{- if .Values.podDisruptionBudget.enabled }}
2+
{{- $pdb := .Values.podDisruptionBudget }}
3+
{{- $hasMin := hasKey $pdb "minAvailable" }}
4+
{{- $hasMax := hasKey $pdb "maxUnavailable" }}
5+
{{- if and $hasMin $hasMax }}
6+
{{- fail "podDisruptionBudget: set minAvailable OR maxUnavailable, not both." }}
7+
{{- end }}
8+
{{- if $hasMin }}
9+
{{- include "karpenter-hetzner.requireInt" (dict "value" $pdb.minAvailable "name" "podDisruptionBudget.minAvailable") }}
10+
{{- if ge (int $pdb.minAvailable) (int .Values.replicas) }}
11+
{{- fail "podDisruptionBudget.minAvailable must be < replicas: minAvailable >= replicas makes the Deployment un-evictable and blocks all voluntary drains." }}
12+
{{- end }}
13+
{{- end }}
14+
{{- if $hasMax }}
15+
{{- include "karpenter-hetzner.requireInt" (dict "value" $pdb.maxUnavailable "name" "podDisruptionBudget.maxUnavailable") }}
16+
{{- end }}
17+
apiVersion: policy/v1
18+
kind: PodDisruptionBudget
19+
metadata:
20+
name: karpenter-provider-hetzner
21+
namespace: {{ .Release.Namespace }}
22+
labels:
23+
app.kubernetes.io/name: karpenter-provider-hetzner
24+
app.kubernetes.io/instance: {{ .Release.Name }}
25+
spec:
26+
{{- if $hasMin }}
27+
minAvailable: {{ $pdb.minAvailable | int }}
28+
{{- else if $hasMax }}
29+
maxUnavailable: {{ $pdb.maxUnavailable | int }}
30+
{{- else }}
31+
maxUnavailable: 1
32+
{{- end }}
33+
selector:
34+
matchLabels:
35+
app.kubernetes.io/name: karpenter-provider-hetzner
36+
{{- end }}

charts/karpenter-provider-hetzner/values.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@ resources:
2525
cpu: 500m
2626
memory: 256Mi
2727

28+
# Pod scheduling & placement. All empty by default — opt in via values.
29+
nodeSelector: {}
30+
affinity: {}
31+
tolerations: []
32+
topologySpreadConstraints: []
33+
imagePullSecrets: []
34+
priorityClassName: ""
35+
36+
# PodDisruptionBudget for voluntary-disruption safety (e.g. node drains). Enable
37+
# when running replicas > 1. Set minAvailable OR maxUnavailable (integers only,
38+
# mutually exclusive — no percentages); leave both unset to default to
39+
# maxUnavailable: 1, which never blocks drains (a single replica can still be
40+
# evicted — run replicas >= 2 to keep the controller available).
41+
podDisruptionBudget:
42+
enabled: false
43+
# minAvailable: 1 # must be < replicas (so needs replicas >= 2)
44+
# maxUnavailable: 1
45+
46+
# Container launch overrides. `args` passes flags to the controller (e.g.
47+
# --log-level, --feature-gates); `command` replaces the entrypoint binary (rarely
48+
# needed — the hardcoded probes expect /healthz and /readyz).
49+
command: []
50+
args: []
51+
2852
# Required: scopes managed servers so multiple clusters can share one Hetzner project.
2953
clusterName: ""
3054

0 commit comments

Comments
 (0)