From f178db5b6cb6fe243b23a7826a7566c7058b48ee Mon Sep 17 00:00:00 2001 From: jcstryker Date: Tue, 24 Jun 2025 08:25:06 -0400 Subject: [PATCH 01/19] feat: initial helm chart migration into headplane upstream Signed-off-by: jcstryker --- kubernetes/headplane/Chart.yaml | 6 + kubernetes/headplane/templates/_helpers.tpl | 7 + .../headplane/templates/deployment.yaml | 161 ++++++++++++++++++ .../headplane/templates/extra-objects.yaml | 8 + .../headplane/configmap-scripts.yaml | 132 ++++++++++++++ .../templates/headplane/configmap.yaml | 39 +++++ .../headplane/templates/headplane/pvc.yaml | 30 ++++ .../templates/headplane/secrets.yaml | 29 ++++ .../templates/headplane/service.yaml | 14 ++ .../headplane/templates/headscale/pvc.yaml | 30 ++++ .../headplane/templates/headscale/secret.yaml | 80 +++++++++ .../templates/headscale/service.yaml | 14 ++ kubernetes/headplane/templates/role.yaml | 17 ++ .../headplane/templates/rolebinding.yaml | 11 ++ .../headplane/templates/serviceaccount.yaml | 5 + kubernetes/headplane/values.yaml | 100 +++++++++++ 16 files changed, 683 insertions(+) create mode 100644 kubernetes/headplane/Chart.yaml create mode 100644 kubernetes/headplane/templates/_helpers.tpl create mode 100644 kubernetes/headplane/templates/deployment.yaml create mode 100644 kubernetes/headplane/templates/extra-objects.yaml create mode 100644 kubernetes/headplane/templates/headplane/configmap-scripts.yaml create mode 100644 kubernetes/headplane/templates/headplane/configmap.yaml create mode 100644 kubernetes/headplane/templates/headplane/pvc.yaml create mode 100644 kubernetes/headplane/templates/headplane/secrets.yaml create mode 100644 kubernetes/headplane/templates/headplane/service.yaml create mode 100644 kubernetes/headplane/templates/headscale/pvc.yaml create mode 100644 kubernetes/headplane/templates/headscale/secret.yaml create mode 100644 kubernetes/headplane/templates/headscale/service.yaml create mode 100644 kubernetes/headplane/templates/role.yaml create mode 100644 kubernetes/headplane/templates/rolebinding.yaml create mode 100644 kubernetes/headplane/templates/serviceaccount.yaml create mode 100644 kubernetes/headplane/values.yaml diff --git a/kubernetes/headplane/Chart.yaml b/kubernetes/headplane/Chart.yaml new file mode 100644 index 00000000..3fe0ed2f --- /dev/null +++ b/kubernetes/headplane/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +appVersion: 0.6.0 +description: Kubernetes Helm Chart for Headplane in integrated mode +name: headplane +type: application +version: 1.0.0 diff --git a/kubernetes/headplane/templates/_helpers.tpl b/kubernetes/headplane/templates/_helpers.tpl new file mode 100644 index 00000000..3ce47d38 --- /dev/null +++ b/kubernetes/headplane/templates/_helpers.tpl @@ -0,0 +1,7 @@ +{{- define "headplane.cookieSecret" -}} +{{- if .Values.headplane.config.cookieSecret.value -}} +{{- .Values.headplane.config.cookieSecret.value -}} +{{- else -}} +{{- randAlphaNum 32 -}} +{{- end -}} +{{- end -}} diff --git a/kubernetes/headplane/templates/deployment.yaml b/kubernetes/headplane/templates/deployment.yaml new file mode 100644 index 00000000..3eac0d6d --- /dev/null +++ b/kubernetes/headplane/templates/deployment.yaml @@ -0,0 +1,161 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headplane-integrated +spec: + replicas: 1 + selector: + matchLabels: + app: headplane-integrated + strategy: + type: Recreate + template: + metadata: + labels: + app: headplane-integrated + annotations: + checksum/configmap-headplane: {{ include (print $.Template.BasePath "/headplane/configmap.yaml") . | sha256sum }} + checksum/secret-headplane: {{ include (print $.Template.BasePath "/headplane/secrets.yaml") . | sha256sum }} + checksum/secret-headscale: {{ include (print $.Template.BasePath "/headscale/secret.yaml") . | sha256sum }} + spec: + shareProcessNamespace: true + serviceAccountName: headplane-integrated + securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: headplane + image: "{{ required "headplane image repository required" .Values.headplane.image.repository }}:{{ .Values.headplane.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ required "headplane image pull policy required" .Values.headplane.image.pullPolicy }} + env: + - name: HEADPLANE_DEBUG_LOG + value: {{ .Values.headplane.config.debug | quote }} + - name: HEADPLANE_LOAD_ENV_OVERRIDES + value: "true" + - name: HEADPLANE_INTEGRATION__KUBERNETES__POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: 'HEADPLANE_SERVER__COOKIE_SECRET' + valueFrom: + secretKeyRef: + name: {{ default "headplane-cookie-secret" .Values.headplane.config.cookieSecret.secretName }} + key: {{ .Values.headplane.config.cookieSecret.secretKey }} + {{- if .Values.headplane.config.oidc.enabled }} + - name: 'HEADPLANE_OIDC__HEADSCALE_API_KEY' + valueFrom: + secretKeyRef: + name: {{ default "headscale-api-key" .Values.headplane.config.oidc.headscaleApiKey.secretName }} + key: {{ .Values.headplane.config.oidc.headscaleApiKey.secretKey }} + {{- end }} + ports: + - name: app + containerPort: 3000 + protocol: TCP + securityContext: {{- toYaml .Values.headplane.securityContext | nindent 12 }} + volumeMounts: + - name: headplane-config + mountPath: /etc/headplane + {{- if .Values.headplane.config.oidc.enabled }} + - name: headplane-oidc-client-secret + mountPath: /var/secrets/headplane/oidc + {{- end }} + - name: headplane-data + mountPath: /var/lib/headplane + readOnly: false + - name: headscale-config + mountPath: /etc/headscale + initContainers: + - name: headscale + image: "{{ required "headscale image repository required" .Values.headscale.image.repository }}:{{ required "headscale image tag required" .Values.headscale.image.tag }}" + imagePullPolicy: {{ required "headscale image pull policy required" .Values.headscale.image.pullPolicy }} + restartPolicy: Always + args: + - serve + ports: + - name: api + containerPort: 8080 + protocol: TCP + - name: metrics + containerPort: 9090 + protocol: TCP + - name: grpc + containerPort: 50443 + protocol: TCP + - name: stun + containerPort: 3478 + protocol: UDP + securityContext: {{- toYaml .Values.headscale.securityContext | nindent 12 }} + volumeMounts: + - name: headscale-config + mountPath: /etc/headscale + {{- if .Values.headscale.config.oidc.enabled }} + - name: headscale-oidc-client-secret + mountPath: /var/secrets/headscale/oidc + {{- end }} + - name: headscale-data + mountPath: /var/lib/headscale + readOnly: false + {{- if .Values.headplane.config.generateCredentials }} + - name: generate-headscale-token + image: alpine/k8s:1.33.1 + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: HEADSCALE_SECRET_NAME + value: {{ default "headscale-api-key" .Values.headplane.config.oidc.headscaleApiKey.secretName }} + - name: HEADSCALE_POD_SELECTOR + value: "app=headplane-integrated" + command: [ "bash", "-c" ] + args: [ "/etc/scripts/ensure-headscale-api-key.sh" ] + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + volumeMounts: + - name: headplane-scripts + mountPath: /etc/scripts + {{- end }} + volumes: + - name: headplane-config + configMap: + name: headplane + {{- if .Values.headplane.config.oidc.enabled }} + - name: headplane-oidc-client-secret + secret: + secretName: {{ default "headplane-oidc-client-secret" .Values.headplane.config.oidc.clientSecret.secretName }} + {{- end }} + - name: headplane-data + {{- if .Values.headplane.persistence.enabled }} + persistentVolumeClaim: + claimName: headplane-data + {{- else }} + emptyDir: + sizeLimit: 500Mi + {{- end }} + {{- if .Values.headplane.config.generateCredentials }} + - name: headplane-scripts + configMap: + name: headplane-scripts + defaultMode: 0777 + {{- end }} + - name: headscale-config + secret: + secretName: headscale + {{- if .Values.headscale.config.oidc.enabled }} + - name: headscale-oidc-client-secret + secret: + secretName: {{ default "headscale-oidc-client-secret" .Values.headscale.config.oidc.clientSecret.secretName }} + {{- end }} + - name: headscale-data + {{- if .Values.headscale.persistence.enabled }} + persistentVolumeClaim: + claimName: headscale-config + {{- else }} + emptyDir: + sizeLimit: 500Mi + {{- end }} diff --git a/kubernetes/headplane/templates/extra-objects.yaml b/kubernetes/headplane/templates/extra-objects.yaml new file mode 100644 index 00000000..fc9a76b8 --- /dev/null +++ b/kubernetes/headplane/templates/extra-objects.yaml @@ -0,0 +1,8 @@ +{{ range .Values.extraObjects }} +--- +{{ if typeIs "string" . }} + {{- tpl . $ }} +{{- else }} + {{- tpl (toYaml .) $ }} +{{- end }} +{{ end }} diff --git a/kubernetes/headplane/templates/headplane/configmap-scripts.yaml b/kubernetes/headplane/templates/headplane/configmap-scripts.yaml new file mode 100644 index 00000000..617d3ef7 --- /dev/null +++ b/kubernetes/headplane/templates/headplane/configmap-scripts.yaml @@ -0,0 +1,132 @@ +{{- if .Values.headplane.config.generateCredentials }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: headplane-scripts +data: + ensure-headscale-api-key.sh: | + set -e + + NAMESPACE="${NAMESPACE:?Error: NAMESPACE environment variable not set.}" + HEADSCALE_SECRET_NAME="${HEADSCALE_SECRET_NAME:?Error: HEADSCALE_SECRET_NAME environment variable not set.}" + HEADSCALE_POD_SELECTOR="${HEADSCALE_POD_SELECTOR:?Error: HEADSCALE_POD_SELECTOR environment variable not set.}" + + check_api_key_validity() { + local key_to_check="$1" + local validation_url="$HEADSCALE_HOST:8080/api/v1/user" + + local http_status + local curl_stderr_output + local curl_exit_status + + curl_stderr_output=$(curl -sS --fail -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $key_to_check" "$validation_url" 2>&1) + curl_exit_status=$? + + http_status=$(echo "$curl_stderr_output" | tail -n 1) + curl_stderr_output=$(echo "$curl_stderr_output" | head -n -1) + + if [[ "$curl_exit_status" -eq 0 && "$http_status" =~ ^2 ]]; then + echo "Headscale API Key is valid (HTTP $http_status OK)." + return 0 + else + echo "API Key validation failed." + echo "Debug Info:" + echo " Validation URL: $validation_url" + echo " Curl Exit Status: $curl_exit_status" + echo " HTTP Status Code: $http_status" + if [[ -n "$curl_stderr_output" ]]; then + echo " Curl Error Output: $curl_stderr_output" + fi + return 1 + fi + } + + echo "Finding headscale pod in namespace '$NAMESPACE' with selector '$HEADSCALE_POD_SELECTOR'..." + HEADPLANE_POD=$(kubectl get pod -n "$NAMESPACE" -l "$HEADSCALE_POD_SELECTOR" -o jsonpath="{.items[0].metadata.name}" --ignore-not-found) + + if [[ -z "$HEADPLANE_POD" ]]; then + echo "Error: No headscale pod found matching selector '$HEADSCALE_POD_SELECTOR' in namespace '$NAMESPACE'." + exit 1 + fi + echo "Success: Found headscale pod '$HEADPLANE_POD'" + + + echo "Checking 'headscale' container status in pod '$HEADPLANE_POD' for readiness..." + + container_ready=$(kubectl get pod -n "$NAMESPACE" "$HEADPLANE_POD" -o jsonpath="{.status.initContainerStatuses[?(@.name==\"headscale\")].ready}" 2>/dev/null || echo "") + + if [[ "$container_ready" == "true" ]]; then + echo "Success: 'headscale' container is ready" + else + echo "--- Headscale Container Readiness Check Failed ---" + echo "Error: 'headscale' container in pod '$HEADPLANE_POD' is NOT ready." + if [[ -z "$container_ready" ]]; then + echo " Reason: Container status not yet available (Pod might be starting or in a pending state)." + else + echo " Reason: Container status found, but reported as '$container_ready'." + fi + echo " Namespace: '$NAMESPACE'" + echo " Pod Name: '$HEADPLANE_POD'" + echo "---------------------------------------------------------" + exit 1 + fi + + if [[ -z "${HEADSCALE_HOST:-}" ]]; then + echo "HEADSCALE_HOST environment variable not provided. Attempting to determine pod IP for pod '$HEADPLANE_POD'..." + POD_IP=$(kubectl get pod -n $NAMESPACE $HEADPLANE_POD -o jsonpath='{.status.podIP}' --ignore-not-found) + + if [[ -z "$POD_IP" ]]; then + POD_IP="127.0.0.1" + echo "Could not retrieve IP for pod '$HEADPLANE_POD'. Using default." + fi + + HEADSCALE_HOST="http://$POD_IP" + echo "HEADSCALE_HOST set to: '$HEADSCALE_HOST'" + fi + + API_KEY="" + echo "Checking for existing Kubernetes secret '$HEADSCALE_SECRET_NAME' in namespace '$NAMESPACE'..." + + ENCODED_KEY_DATA=$(kubectl get secret "$HEADSCALE_SECRET_NAME" -n "$NAMESPACE" -o=jsonpath='{.data.api-key}' --ignore-not-found 2>/dev/null || echo "") + + if [[ -n "$ENCODED_KEY_DATA" ]]; then + API_KEY=$(echo "$ENCODED_KEY_DATA" | base64 -d) + echo "Existing Headscale API Key found in secret '$HEADSCALE_SECRET_NAME'." + + if check_api_key_validity "$API_KEY"; then + echo "Existing Headscale API Key is valid. No further action needed." + exit 0 + else + echo "Existing Headscale API Key is invalid. A new API Key will be generated." + fi + else + echo "Kubernetes secret '$HEADSCALE_SECRET_NAME' not found or does not contain a 'api-key' field. A new API Key will be generated." + fi + + echo "Generating a new Headscale API Key by executing CLI inside 'headscale' container..." + API_KEY=$(kubectl exec -n "$NAMESPACE" -c headscale "$HEADPLANE_POD" -- headscale apikeys create -e 100y) + + if [[ -z "$API_KEY" ]]; then + echo "Error: Failed to create a new API Key via 'headscale apikeys create' command." + exit 1 + fi + echo "Successfully generated a new Headscale API Key." + + if kubectl get secret "$HEADSCALE_SECRET_NAME" -n "$NAMESPACE" &>/dev/null; then + echo "Updating existing secret '$HEADSCALE_SECRET_NAME' with the new API Key..." + kubectl patch secret "$HEADSCALE_SECRET_NAME" -n "$NAMESPACE" -p "{\"stringData\":{\"api-key\":\"$API_KEY\"}}" --type=merge + else + echo "Creating new secret '$HEADSCALE_SECRET_NAME' with the new API Key..." + kubectl create secret generic "$HEADSCALE_SECRET_NAME" -n "$NAMESPACE" --from-literal="api-key=$API_KEY" + fi + echo "Successfully ensured Headscale API Key in Kubernetes secret '$HEADSCALE_SECRET_NAME'." + + echo "--- Performing final validation of the newly generated API Key ---" + if check_api_key_validity "$API_KEY"; then + echo "Final validation successful: The newly generated and stored Headscale API Key is valid." + exit 0 + else + echo "Final validation failed: The newly generated/stored Headscale API Key is NOT valid. Please investigate." + exit 1 + fi +{{- end }} \ No newline at end of file diff --git a/kubernetes/headplane/templates/headplane/configmap.yaml b/kubernetes/headplane/templates/headplane/configmap.yaml new file mode 100644 index 00000000..2c7f0168 --- /dev/null +++ b/kubernetes/headplane/templates/headplane/configmap.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: headplane +data: + config.yaml: | + server: + host: 0.0.0.0 + port: 3000 + cookie_secret: replaced-by-environment-variable + cookie_secure: true + headscale: + url: http://127.0.0.1:8080 + {{- if .Values.headscale.url }} + public_url: {{ .Values.headscale.config.url }} + {{- end }} + config_path: /etc/headscale/config.yaml + config_strict: true + integration: + agent: + enabled: false + docker: + enabled: false + kubernetes: + enabled: true + validate_manifest: true + pod_name: replaced-by-environment-variable + proc: + enabled: false + {{- if .Values.headplane.config.oidc.enabled }} + oidc: + issuer: {{ .Values.headplane.config.oidc.issuerUrl }} + client_id: {{ .Values.headplane.config.oidc.clientId }} + client_secret_path: /var/secrets/headplane/oidc/client-secret + disable_api_key_login: {{ .Values.headplane.config.oidc.disableApiKeyLogin }} + token_endpoint_auth_method: {{ .Values.headplane.config.oidc.tokenEndpointAuthMethod }} + headscale_api_key: replaced-by-environment-variable + redirect_uri: {{ .Values.headplane.config.url }}/admin/oidc/callback + {{- end }} diff --git a/kubernetes/headplane/templates/headplane/pvc.yaml b/kubernetes/headplane/templates/headplane/pvc.yaml new file mode 100644 index 00000000..fa5e632f --- /dev/null +++ b/kubernetes/headplane/templates/headplane/pvc.yaml @@ -0,0 +1,30 @@ +{{- if .Values.headplane.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Values.pvc.name | quote }} + {{- if .Values.pvc.annotations }} + annotations: + {{- range $key, $value := .Values.pvc.annotations }} + {{ $key | quote }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{- if .Values.pvc.labels }} + labels: + {{- range $key, $value := .Values.pvc.labels }} + {{ $key | quote }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + accessModes: + {{- range .Values.pvc.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.pvc.storage | quote }} + {{- if .Values.pvc.storageClassName }} + storageClassName: {{ .Values.pvc.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/kubernetes/headplane/templates/headplane/secrets.yaml b/kubernetes/headplane/templates/headplane/secrets.yaml new file mode 100644 index 00000000..3fe4143f --- /dev/null +++ b/kubernetes/headplane/templates/headplane/secrets.yaml @@ -0,0 +1,29 @@ +{{ if not .Values.headplane.config.cookieSecret.secretName }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: headplane-cookie-secret +stringData: + cookie-secret: {{ include "headplane.cookieSecret" . }} +{{- end }} +{{- if and .Values.headplane.config.oidc.enabled }} +{{- if not .Values.headplane.config.oidc.clientSecret.secretName }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: headplane-oidc-client-secret +stringData: + client-secret: {{ required "headplane plaintext client secret value required" .Values.headplane.config.oidc.clientSecret.value }} +{{- end }} +{{- if (and (not .Values.headplane.config.oidc.headscaleApiKey.secretName) (not .Values.headplane.config.generateCredentials)) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: headscale-api-key +stringData: + api-key: {{ required "headplane plaintext headscale api key required" .Values.headplane.config.oidc.headscaleApiKey.value }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/kubernetes/headplane/templates/headplane/service.yaml b/kubernetes/headplane/templates/headplane/service.yaml new file mode 100644 index 00000000..ae64b017 --- /dev/null +++ b/kubernetes/headplane/templates/headplane/service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: headplane +spec: + ports: + - name: headplane-ui + port: 80 + protocol: TCP + targetPort: app + selector: + app: headplane-integrated + type: ClusterIP diff --git a/kubernetes/headplane/templates/headscale/pvc.yaml b/kubernetes/headplane/templates/headscale/pvc.yaml new file mode 100644 index 00000000..72798055 --- /dev/null +++ b/kubernetes/headplane/templates/headscale/pvc.yaml @@ -0,0 +1,30 @@ +{{- if .Values.headscale.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Values.pvc.name | quote }} + {{- if .Values.pvc.annotations }} + annotations: + {{- range $key, $value := .Values.pvc.annotations }} + {{ $key | quote }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{- if .Values.pvc.labels }} + labels: + {{- range $key, $value := .Values.pvc.labels }} + {{ $key | quote }}: {{ $value | quote }} + {{- end }} + {{- end }} +spec: + accessModes: + {{- range .Values.pvc.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.pvc.storage | quote }} + {{- if .Values.pvc.storageClassName }} + storageClassName: {{ .Values.pvc.storageClassName | quote }} + {{- end }} +{{- end }} diff --git a/kubernetes/headplane/templates/headscale/secret.yaml b/kubernetes/headplane/templates/headscale/secret.yaml new file mode 100644 index 00000000..4b7c2c92 --- /dev/null +++ b/kubernetes/headplane/templates/headscale/secret.yaml @@ -0,0 +1,80 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: headscale +type: Opaque +stringData: + config.yaml: | + server_url: http://localhost:8080 + listen_addr: 0.0.0.0:8080 + metrics_listen_addr: 127.0.0.1:9090 + grpc_listen_addr: 127.0.0.1:50443 + grpc_allow_insecure: false + noise: + private_key_path: /var/lib/headscale/noise_private.key + prefixes: + v4: 100.64.0.0/10 + v6: fd7a:115c:a1e0::/48 + allocation: sequential + derp: + server: + enabled: false + urls: + - https://controlplane.tailscale.com/derpmap/default + paths: [] + auto_update_enabled: true + update_frequency: 24h + disable_check_updates: true + ephemeral_node_inactivity_timeout: 30m + database: + type: sqlite + gorm: + prepare_stmt: true + parameterized_queries: true + skip_err_record_not_found: true + slow_threshold: 1000 + sqlite: + path: /var/lib/headscale/db.sqlite + write_ahead_log: true + wal_autocheckpoint: 1000 + log: + format: text + level: info + policy: + mode: database + dns: + magic_dns: true + base_domain: example.com + nameservers: + global: + - 1.1.1.1 + - 1.0.0.1 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001 + unix_socket: /var/lib/headscale/headscale.sock + unix_socket_permission: "0770" + {{- if .Values.headscale.config.oidc.enabled }} + oidc: + only_start_if_oidc_is_available: {{ .Values.headscale.config.oidc.startupCheck }} + issuer: {{ .Values.headscale.config.oidc.issuerUrl }} + client_id: {{ .Values.headscale.config.oidc.clientId }} + client_secret_path: /var/secrets/headscale/oidc/client-secret + expiry: 180d + use_expiry_from_token: false + pkce: + enabled: {{ .Values.headscale.config.oidc.pkceEnabled }} + method: S256 + {{- end }} + logtail: + enabled: false + randomize_client_port: false +{{ if and .Values.headscale.config.oidc.enabled (not .Values.headscale.config.oidc.clientSecret.secretName) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: headscale-oidc-client-secret +stringData: + client-secret: {{ required "headscale plaintext secret value required" .Values.headscale.config.oidc.clientSecret.value }} +{{- end }} \ No newline at end of file diff --git a/kubernetes/headplane/templates/headscale/service.yaml b/kubernetes/headplane/templates/headscale/service.yaml new file mode 100644 index 00000000..70b89413 --- /dev/null +++ b/kubernetes/headplane/templates/headscale/service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: headscale +spec: + ports: + - name: headscale-api + port: 80 + protocol: TCP + targetPort: api + selector: + app: headplane-integrated + type: ClusterIP diff --git a/kubernetes/headplane/templates/role.yaml b/kubernetes/headplane/templates/role.yaml new file mode 100644 index 00000000..42bb327f --- /dev/null +++ b/kubernetes/headplane/templates/role.yaml @@ -0,0 +1,17 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: headplane-integrated +rules: +- apiGroups: ['apps'] + resources: ['deployments'] + verbs: ['get', 'list'] +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] +- apiGroups: [""] + resources: ["pods/exec"] + verbs: ["create"] +- apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "patch", "get"] \ No newline at end of file diff --git a/kubernetes/headplane/templates/rolebinding.yaml b/kubernetes/headplane/templates/rolebinding.yaml new file mode 100644 index 00000000..888c060e --- /dev/null +++ b/kubernetes/headplane/templates/rolebinding.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: headplane-integrated +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: headplane-integrated +subjects: +- kind: ServiceAccount + name: headplane-integrated \ No newline at end of file diff --git a/kubernetes/headplane/templates/serviceaccount.yaml b/kubernetes/headplane/templates/serviceaccount.yaml new file mode 100644 index 00000000..6d5c4f1f --- /dev/null +++ b/kubernetes/headplane/templates/serviceaccount.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headplane-integrated \ No newline at end of file diff --git a/kubernetes/headplane/values.yaml b/kubernetes/headplane/values.yaml new file mode 100644 index 00000000..246b5980 --- /dev/null +++ b/kubernetes/headplane/values.yaml @@ -0,0 +1,100 @@ +headplane: + image: + repository: ghcr.io/tale/headplane + pullPolicy: IfNotPresent + tag: "" + config: + url: https://headplane.example.com + debug: false + generateCredentials: false + cookieSecret: + value: "" + secretName: ~ + secretKey: "cookie-secret" + oidc: + enabled: false + issuerUrl: "" + disableApiKeyLogin: false + tokenEndpointAuthMethod: client_secret_post + redirectUri: "" + clientId: "" + clientSecret: + value: "" + secretName: ~ + secretKey: "client-secret" + headscaleApiKey: + value: "" + secretName: ~ + secretKey: "api-key" + persistence: + enabled: false + pvc: + enabled: false + name: headplane-data + accessModes: + - ReadWriteOnce + storage: 1Gi + annotations: {} + labels: [] + storageClassName: ~ + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + ingress: + enabled: false + className: "" + annotations: [] + labels: [] + gateway: + enabled: false +headscale: + image: + repository: headscale/headscale + pullPolicy: IfNotPresent + tag: "0.25.1" + config: + url: https://headscale.example.com + dns: + magicDns: true + baseDomain: example.com + nameservers: + global: + - 9.9.9.9 + - 149.112.112.112 + - 2620:fe::fe + - 2620:fe::9 + oidc: + enabled: true + issuerUrl: "" + startupCheck: false + pkceEnabled: true + clientId: "" + clientSecret: + value: "" + secretName: ~ + secretKey: "client-secret" + persistence: + enabled: false + pvc: + enabled: false + name: headscale-data + accessModes: + - ReadWriteOnce + storage: 1Gi + annotations: {} + labels: [] + storageClassName: ~ + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 +podSecurityContext: + fsGroup: 2000 +extraObjects: [] From 9a7c7eaced454840ca76f001323b757a3be75298 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Fri, 4 Jul 2025 10:49:38 -0400 Subject: [PATCH 02/19] feat: add support for hostAliases in deployment Signed-off-by: jcstryker --- kubernetes/headplane/templates/deployment.yaml | 1 + kubernetes/headplane/values.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/kubernetes/headplane/templates/deployment.yaml b/kubernetes/headplane/templates/deployment.yaml index 3eac0d6d..62d23106 100644 --- a/kubernetes/headplane/templates/deployment.yaml +++ b/kubernetes/headplane/templates/deployment.yaml @@ -19,6 +19,7 @@ spec: checksum/secret-headplane: {{ include (print $.Template.BasePath "/headplane/secrets.yaml") . | sha256sum }} checksum/secret-headscale: {{ include (print $.Template.BasePath "/headscale/secret.yaml") . | sha256sum }} spec: + hostAliases: {{- toYaml .Values.hostAliases | nindent 8 }} shareProcessNamespace: true serviceAccountName: headplane-integrated securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} diff --git a/kubernetes/headplane/values.yaml b/kubernetes/headplane/values.yaml index 246b5980..9db388b8 100644 --- a/kubernetes/headplane/values.yaml +++ b/kubernetes/headplane/values.yaml @@ -98,3 +98,4 @@ headscale: podSecurityContext: fsGroup: 2000 extraObjects: [] +hostAliases: [] From eb50611e27ab17a6aee6e9fe153bf29db2458ee7 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Fri, 4 Jul 2025 11:32:24 -0400 Subject: [PATCH 03/19] fix: check for public headscale url Signed-off-by: jcstryker --- kubernetes/headplane/templates/headplane/configmap.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/headplane/templates/headplane/configmap.yaml b/kubernetes/headplane/templates/headplane/configmap.yaml index 2c7f0168..cad21c5d 100644 --- a/kubernetes/headplane/templates/headplane/configmap.yaml +++ b/kubernetes/headplane/templates/headplane/configmap.yaml @@ -11,7 +11,7 @@ data: cookie_secure: true headscale: url: http://127.0.0.1:8080 - {{- if .Values.headscale.url }} + {{- if .Values.headscale.config.url }} public_url: {{ .Values.headscale.config.url }} {{- end }} config_path: /etc/headscale/config.yaml From bca3c77082cb77b3e5047223444ca7ee9fc07200 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Fri, 4 Jul 2025 12:38:18 -0400 Subject: [PATCH 04/19] feat: move headscale config to configmap Signed-off-by: jcstryker --- .../headplane/templates/deployment.yaml | 4 +- .../templates/headscale/configmap.yaml | 72 +++++++++++++++++++ .../headplane/templates/headscale/secret.yaml | 72 ------------------- 3 files changed, 74 insertions(+), 74 deletions(-) create mode 100644 kubernetes/headplane/templates/headscale/configmap.yaml diff --git a/kubernetes/headplane/templates/deployment.yaml b/kubernetes/headplane/templates/deployment.yaml index 62d23106..a41b2742 100644 --- a/kubernetes/headplane/templates/deployment.yaml +++ b/kubernetes/headplane/templates/deployment.yaml @@ -145,8 +145,8 @@ spec: defaultMode: 0777 {{- end }} - name: headscale-config - secret: - secretName: headscale + configMap: + name: headscale {{- if .Values.headscale.config.oidc.enabled }} - name: headscale-oidc-client-secret secret: diff --git a/kubernetes/headplane/templates/headscale/configmap.yaml b/kubernetes/headplane/templates/headscale/configmap.yaml new file mode 100644 index 00000000..df49ecbb --- /dev/null +++ b/kubernetes/headplane/templates/headscale/configmap.yaml @@ -0,0 +1,72 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: headscale +data: + config.yaml: | + server_url: {{ .Values.headscale.config.url }} + listen_addr: 0.0.0.0:8080 + metrics_listen_addr: 127.0.0.1:9090 + grpc_listen_addr: 127.0.0.1:50443 + grpc_allow_insecure: false + noise: + private_key_path: /var/lib/headscale/noise_private.key + prefixes: + v4: 100.64.0.0/10 + v6: fd7a:115c:a1e0::/48 + allocation: sequential + derp: + server: + enabled: false + urls: + - https://controlplane.tailscale.com/derpmap/default + paths: [] + auto_update_enabled: true + update_frequency: 24h + disable_check_updates: true + ephemeral_node_inactivity_timeout: 30m + database: + type: sqlite + gorm: + prepare_stmt: true + parameterized_queries: true + skip_err_record_not_found: true + slow_threshold: 1000 + sqlite: + path: /var/lib/headscale/db.sqlite + write_ahead_log: true + wal_autocheckpoint: 1000 + log: + format: text + level: info + policy: + mode: database + dns: + magic_dns: {{ .Values.headscale.config.dns.magicDns }} + base_domain: {{ .Values.headscale.config.dns.baseDomain }} + {{- if .Values.headscale.config.dns.nameservers }} + nameservers: + {{- if .Values.headscale.config.dns.nameservers.global }} + global: {{- toYaml .Values.headscale.config.dns.nameservers.global | nindent 10 }} + {{- end }} + {{- if .Values.headscale.config.dns.nameservers.split }} + split: {{- toYaml .Values.headscale.config.dns.nameservers.split | nindent 10 }} + {{- end }} + {{- end }} + unix_socket: /var/lib/headscale/headscale.sock + unix_socket_permission: "0770" + {{- if .Values.headscale.config.oidc.enabled }} + oidc: + only_start_if_oidc_is_available: {{ .Values.headscale.config.oidc.startupCheck }} + issuer: {{ .Values.headscale.config.oidc.issuerUrl }} + client_id: {{ .Values.headscale.config.oidc.clientId }} + client_secret_path: /var/secrets/headscale/oidc/client-secret + expiry: 180d + use_expiry_from_token: false + pkce: + enabled: {{ .Values.headscale.config.oidc.pkceEnabled }} + method: S256 + {{- end }} + logtail: + enabled: false + randomize_client_port: false diff --git a/kubernetes/headplane/templates/headscale/secret.yaml b/kubernetes/headplane/templates/headscale/secret.yaml index 4b7c2c92..3af0f77c 100644 --- a/kubernetes/headplane/templates/headscale/secret.yaml +++ b/kubernetes/headplane/templates/headscale/secret.yaml @@ -1,76 +1,4 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - name: headscale -type: Opaque -stringData: - config.yaml: | - server_url: http://localhost:8080 - listen_addr: 0.0.0.0:8080 - metrics_listen_addr: 127.0.0.1:9090 - grpc_listen_addr: 127.0.0.1:50443 - grpc_allow_insecure: false - noise: - private_key_path: /var/lib/headscale/noise_private.key - prefixes: - v4: 100.64.0.0/10 - v6: fd7a:115c:a1e0::/48 - allocation: sequential - derp: - server: - enabled: false - urls: - - https://controlplane.tailscale.com/derpmap/default - paths: [] - auto_update_enabled: true - update_frequency: 24h - disable_check_updates: true - ephemeral_node_inactivity_timeout: 30m - database: - type: sqlite - gorm: - prepare_stmt: true - parameterized_queries: true - skip_err_record_not_found: true - slow_threshold: 1000 - sqlite: - path: /var/lib/headscale/db.sqlite - write_ahead_log: true - wal_autocheckpoint: 1000 - log: - format: text - level: info - policy: - mode: database - dns: - magic_dns: true - base_domain: example.com - nameservers: - global: - - 1.1.1.1 - - 1.0.0.1 - - 2606:4700:4700::1111 - - 2606:4700:4700::1001 - unix_socket: /var/lib/headscale/headscale.sock - unix_socket_permission: "0770" - {{- if .Values.headscale.config.oidc.enabled }} - oidc: - only_start_if_oidc_is_available: {{ .Values.headscale.config.oidc.startupCheck }} - issuer: {{ .Values.headscale.config.oidc.issuerUrl }} - client_id: {{ .Values.headscale.config.oidc.clientId }} - client_secret_path: /var/secrets/headscale/oidc/client-secret - expiry: 180d - use_expiry_from_token: false - pkce: - enabled: {{ .Values.headscale.config.oidc.pkceEnabled }} - method: S256 - {{- end }} - logtail: - enabled: false - randomize_client_port: false {{ if and .Values.headscale.config.oidc.enabled (not .Values.headscale.config.oidc.clientSecret.secretName) }} ---- apiVersion: v1 kind: Secret metadata: From 368cdf4b7b142eca9d75873202a07f55a1966bb8 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Fri, 4 Jul 2025 12:44:26 -0400 Subject: [PATCH 05/19] feat: add empty split dns map Signed-off-by: jcstryker --- kubernetes/headplane/values.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/kubernetes/headplane/values.yaml b/kubernetes/headplane/values.yaml index 9db388b8..dc12ae6c 100644 --- a/kubernetes/headplane/values.yaml +++ b/kubernetes/headplane/values.yaml @@ -67,6 +67,7 @@ headscale: - 149.112.112.112 - 2620:fe::fe - 2620:fe::9 + split: {} oidc: enabled: true issuerUrl: "" From f56fbf853cd1e5b7088af4454d55202aff1204a6 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Fri, 4 Jul 2025 12:54:39 -0400 Subject: [PATCH 06/19] chore: enable startup api check by default Signed-off-by: jcstryker --- kubernetes/headplane/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/headplane/values.yaml b/kubernetes/headplane/values.yaml index dc12ae6c..c79baa7d 100644 --- a/kubernetes/headplane/values.yaml +++ b/kubernetes/headplane/values.yaml @@ -71,7 +71,7 @@ headscale: oidc: enabled: true issuerUrl: "" - startupCheck: false + startupCheck: true pkceEnabled: true clientId: "" clientSecret: From 314cac0e808e8b7b394045cb22c1cc0eae650478 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Thu, 2 Oct 2025 08:15:59 -0400 Subject: [PATCH 07/19] fix(kubernetes): remove unused pvc templates This commit removes the unused PersistentVolumeClaim templates for headplane and headscale components. The templates were left in place but contained only a TODO comment, indicating they were not yet implemented. Removing them cleans up the codebase and prevents confusion about their intended purpose. The pvc templates were previously defined in the kubernetes/headplane/templates/headplane/pvc.yaml and kubernetes/headplane/templates/headscale/pvc.yaml files but were not actually needed for the current implementation. This change reduces code clutter and improves maintainability by removing dead code. Signed-off-by: jcstryker --- .../headplane/templates/headplane/pvc.yaml | 29 +------------------ .../headplane/templates/headscale/pvc.yaml | 29 +------------------ 2 files changed, 2 insertions(+), 56 deletions(-) diff --git a/kubernetes/headplane/templates/headplane/pvc.yaml b/kubernetes/headplane/templates/headplane/pvc.yaml index fa5e632f..582dd4d9 100644 --- a/kubernetes/headplane/templates/headplane/pvc.yaml +++ b/kubernetes/headplane/templates/headplane/pvc.yaml @@ -1,30 +1,3 @@ {{- if .Values.headplane.persistence.enabled }} ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ .Values.pvc.name | quote }} - {{- if .Values.pvc.annotations }} - annotations: - {{- range $key, $value := .Values.pvc.annotations }} - {{ $key | quote }}: {{ $value | quote }} - {{- end }} - {{- end }} - {{- if .Values.pvc.labels }} - labels: - {{- range $key, $value := .Values.pvc.labels }} - {{ $key | quote }}: {{ $value | quote }} - {{- end }} - {{- end }} -spec: - accessModes: - {{- range .Values.pvc.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.pvc.storage | quote }} - {{- if .Values.pvc.storageClassName }} - storageClassName: {{ .Values.pvc.storageClassName | quote }} - {{- end }} +TODO {{- end }} diff --git a/kubernetes/headplane/templates/headscale/pvc.yaml b/kubernetes/headplane/templates/headscale/pvc.yaml index 72798055..9866f1c2 100644 --- a/kubernetes/headplane/templates/headscale/pvc.yaml +++ b/kubernetes/headplane/templates/headscale/pvc.yaml @@ -1,30 +1,3 @@ {{- if .Values.headscale.persistence.enabled }} ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ .Values.pvc.name | quote }} - {{- if .Values.pvc.annotations }} - annotations: - {{- range $key, $value := .Values.pvc.annotations }} - {{ $key | quote }}: {{ $value | quote }} - {{- end }} - {{- end }} - {{- if .Values.pvc.labels }} - labels: - {{- range $key, $value := .Values.pvc.labels }} - {{ $key | quote }}: {{ $value | quote }} - {{- end }} - {{- end }} -spec: - accessModes: - {{- range .Values.pvc.accessModes }} - - {{ . | quote }} - {{- end }} - resources: - requests: - storage: {{ .Values.pvc.storage | quote }} - {{- if .Values.pvc.storageClassName }} - storageClassName: {{ .Values.pvc.storageClassName | quote }} - {{- end }} +TODO {{- end }} From 4942ede5e8b979a0d652708b285547fc86cc434c Mon Sep 17 00:00:00 2001 From: jcstryker Date: Mon, 13 Oct 2025 08:55:12 -0400 Subject: [PATCH 08/19] bump versions Signed-off-by: jcstryker --- kubernetes/headplane/Chart.yaml | 2 +- kubernetes/headplane/values.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kubernetes/headplane/Chart.yaml b/kubernetes/headplane/Chart.yaml index 3fe0ed2f..792a648f 100644 --- a/kubernetes/headplane/Chart.yaml +++ b/kubernetes/headplane/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -appVersion: 0.6.0 +appVersion: 0.6.1 description: Kubernetes Helm Chart for Headplane in integrated mode name: headplane type: application diff --git a/kubernetes/headplane/values.yaml b/kubernetes/headplane/values.yaml index c79baa7d..4b90d17f 100644 --- a/kubernetes/headplane/values.yaml +++ b/kubernetes/headplane/values.yaml @@ -55,7 +55,7 @@ headscale: image: repository: headscale/headscale pullPolicy: IfNotPresent - tag: "0.25.1" + tag: "0.26.1" config: url: https://headscale.example.com dns: From 73e717303cfb8ff0676ae9c6a2e304496b048f16 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Mon, 13 Oct 2025 10:09:24 -0400 Subject: [PATCH 09/19] feat: support new secret passing paths Signed-off-by: jcstryker --- .../headplane/templates/deployment.yaml | 28 +++++++++++-------- .../templates/headplane/configmap.yaml | 12 ++------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/kubernetes/headplane/templates/deployment.yaml b/kubernetes/headplane/templates/deployment.yaml index a41b2742..9c13537f 100644 --- a/kubernetes/headplane/templates/deployment.yaml +++ b/kubernetes/headplane/templates/deployment.yaml @@ -36,18 +36,6 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name - - name: 'HEADPLANE_SERVER__COOKIE_SECRET' - valueFrom: - secretKeyRef: - name: {{ default "headplane-cookie-secret" .Values.headplane.config.cookieSecret.secretName }} - key: {{ .Values.headplane.config.cookieSecret.secretKey }} - {{- if .Values.headplane.config.oidc.enabled }} - - name: 'HEADPLANE_OIDC__HEADSCALE_API_KEY' - valueFrom: - secretKeyRef: - name: {{ default "headscale-api-key" .Values.headplane.config.oidc.headscaleApiKey.secretName }} - key: {{ .Values.headplane.config.oidc.headscaleApiKey.secretKey }} - {{- end }} ports: - name: app containerPort: 3000 @@ -56,9 +44,13 @@ spec: volumeMounts: - name: headplane-config mountPath: /etc/headplane + - name: headplane-server-cookie-secret + mountPath: /var/secrets/headplane/server {{- if .Values.headplane.config.oidc.enabled }} - name: headplane-oidc-client-secret mountPath: /var/secrets/headplane/oidc + - name: headplane-oidc-headscale-api-key + mountPath: /var/secrets/headplane/headscale {{- end }} - name: headplane-data mountPath: /var/lib/headplane @@ -122,13 +114,22 @@ spec: mountPath: /etc/scripts {{- end }} volumes: + ### + ### Headplane Volumes ### + ### - name: headplane-config configMap: name: headplane + - name: headplane-server-cookie-secret + secret: + secretName: {{ default "headplane-cookie-secret" .Values.headplane.config.cookieSecret.secretName }} {{- if .Values.headplane.config.oidc.enabled }} - name: headplane-oidc-client-secret secret: secretName: {{ default "headplane-oidc-client-secret" .Values.headplane.config.oidc.clientSecret.secretName }} + - name: headplane-oidc-headscale-api-key + secret: + secretName: {{ default "headscale-api-key" .Values.headplane.config.oidc.headscaleApiKey.secretName }} {{- end }} - name: headplane-data {{- if .Values.headplane.persistence.enabled }} @@ -144,6 +145,9 @@ spec: name: headplane-scripts defaultMode: 0777 {{- end }} + ### + ### Headscale Volumes ### + ### - name: headscale-config configMap: name: headscale diff --git a/kubernetes/headplane/templates/headplane/configmap.yaml b/kubernetes/headplane/templates/headplane/configmap.yaml index cad21c5d..24452a45 100644 --- a/kubernetes/headplane/templates/headplane/configmap.yaml +++ b/kubernetes/headplane/templates/headplane/configmap.yaml @@ -7,7 +7,7 @@ data: server: host: 0.0.0.0 port: 3000 - cookie_secret: replaced-by-environment-variable + cookie_secret_path: /var/secrets/headplane/server/{{ .Values.headplane.config.cookieSecret.secretKey }} cookie_secure: true headscale: url: http://127.0.0.1:8080 @@ -17,23 +17,17 @@ data: config_path: /etc/headscale/config.yaml config_strict: true integration: - agent: - enabled: false - docker: - enabled: false kubernetes: enabled: true validate_manifest: true pod_name: replaced-by-environment-variable - proc: - enabled: false {{- if .Values.headplane.config.oidc.enabled }} oidc: issuer: {{ .Values.headplane.config.oidc.issuerUrl }} client_id: {{ .Values.headplane.config.oidc.clientId }} - client_secret_path: /var/secrets/headplane/oidc/client-secret + client_secret_path: /var/secrets/headplane/oidc/{{ .Values.headplane.config.oidc.clientSecret.secretKey }} disable_api_key_login: {{ .Values.headplane.config.oidc.disableApiKeyLogin }} token_endpoint_auth_method: {{ .Values.headplane.config.oidc.tokenEndpointAuthMethod }} - headscale_api_key: replaced-by-environment-variable + headscale_api_key_path: /var/secrets/headplane/headscale/{{ .Values.headplane.config.oidc.headscaleApiKey.secretKey }} redirect_uri: {{ .Values.headplane.config.url }}/admin/oidc/callback {{- end }} From a01e21a8889371ed8d92e498723ce49c9f6d9a43 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Mon, 13 Oct 2025 12:12:51 -0400 Subject: [PATCH 10/19] feat(kubernetes): add persistent volume claims for headplane and headscale This commit introduces the missing PersistentVolumeClaim templates for both headplane and headscale components. The changes implement the full PVC specification with support for: - Custom annotations and labels - Configurable access modes - Storage requests - Storage class configuration The templates follow the existing pattern used in the helm chart and are conditionally enabled based on the persistence configuration values. This provides the necessary storage persistence for both services when enabled. The implementation includes proper templating with Helm syntax to ensure compatibility with the chart's configuration system. Signed-off-by: jcstryker --- .../headplane/templates/headplane/pvc.yaml | 22 ++++++++++++++++++- .../headplane/templates/headscale/pvc.yaml | 22 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/kubernetes/headplane/templates/headplane/pvc.yaml b/kubernetes/headplane/templates/headplane/pvc.yaml index 582dd4d9..f1082391 100644 --- a/kubernetes/headplane/templates/headplane/pvc.yaml +++ b/kubernetes/headplane/templates/headplane/pvc.yaml @@ -1,3 +1,23 @@ {{- if .Values.headplane.persistence.enabled }} -TODO +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: headplane-data + {{- with .Values.headplane.persistence.pvc.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.headplane.persistence.pvc.labels }} + labels: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- toYaml .Values.headplane.persistence.pvc.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.headplane.persistence.pvc.storage }} + {{- if .Values.headplane.persistence.pvc.storageClassName }} + storageClassName: {{ .Values.headplane.persistence.pvc.storageClassName }} + {{- end }} {{- end }} diff --git a/kubernetes/headplane/templates/headscale/pvc.yaml b/kubernetes/headplane/templates/headscale/pvc.yaml index 9866f1c2..00685c4a 100644 --- a/kubernetes/headplane/templates/headscale/pvc.yaml +++ b/kubernetes/headplane/templates/headscale/pvc.yaml @@ -1,3 +1,23 @@ {{- if .Values.headscale.persistence.enabled }} -TODO +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: headscale-data + {{- with .Values.headscale.persistence.pvc.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.headscale.persistence.pvc.labels }} + labels: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- toYaml .Values.headscale.persistence.pvc.accessModes | nindent 4 }} + resources: + requests: + storage: {{ .Values.headscale.persistence.pvc.storage }} + {{- if .Values.headscale.persistence.pvc.storageClassName }} + storageClassName: {{ .Values.headscale.persistence.pvc.storageClassName }} + {{- end }} {{- end }} From d54fa0747b8bac5fda69c395e7355d68966b5b7c Mon Sep 17 00:00:00 2001 From: jcstryker Date: Mon, 13 Oct 2025 12:17:30 -0400 Subject: [PATCH 11/19] fix(kubernetes): correct persistent volume claim name for headscale-data Signed-off-by: jcstryker --- kubernetes/headplane/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/headplane/templates/deployment.yaml b/kubernetes/headplane/templates/deployment.yaml index 9c13537f..8d07fc9b 100644 --- a/kubernetes/headplane/templates/deployment.yaml +++ b/kubernetes/headplane/templates/deployment.yaml @@ -159,7 +159,7 @@ spec: - name: headscale-data {{- if .Values.headscale.persistence.enabled }} persistentVolumeClaim: - claimName: headscale-config + claimName: headscale-data {{- else }} emptyDir: sizeLimit: 500Mi From 6739dd6b96791a3241cdd20febe3f1e16c760e08 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:10:44 -0500 Subject: [PATCH 12/19] feat: test github action + match semver Signed-off-by: jcstryker --- .github/workflows/chart-publish.yaml | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/chart-publish.yaml diff --git a/.github/workflows/chart-publish.yaml b/.github/workflows/chart-publish.yaml new file mode 100644 index 00000000..a7bb6fef --- /dev/null +++ b/.github/workflows/chart-publish.yaml @@ -0,0 +1,34 @@ +name: Chart Publish +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + packages: write + contents: read + +jobs: + publish-chart: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Helm + uses: azure/setup-helm@v3 + + - name: Lint Helm chart + run: helm lint ./kubernetes/headplane + + - name: Login to GHCR + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push Helm chart to GHCR OCI repository + run: | + helm push ./kubernetes/headplane oci://ghcr.io/${{ github.repository }}/charts/${{ github.repository }} From 7372473888aa1f21ab155d73ba51e293d0acb024 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:11:02 -0500 Subject: [PATCH 13/19] fix: match semver Signed-off-by: jcstryker --- kubernetes/headplane/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kubernetes/headplane/Chart.yaml b/kubernetes/headplane/Chart.yaml index 792a648f..d58657f7 100644 --- a/kubernetes/headplane/Chart.yaml +++ b/kubernetes/headplane/Chart.yaml @@ -3,4 +3,4 @@ appVersion: 0.6.1 description: Kubernetes Helm Chart for Headplane in integrated mode name: headplane type: application -version: 1.0.0 +version: 0.6.1 From 9c08746831cbaa83ddfb8354108bb51120352060 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:13:19 -0500 Subject: [PATCH 14/19] feat: allow test branch workflow Signed-off-by: jcstryker --- .github/workflows/chart-publish.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/chart-publish.yaml b/.github/workflows/chart-publish.yaml index a7bb6fef..a8cf25b9 100644 --- a/.github/workflows/chart-publish.yaml +++ b/.github/workflows/chart-publish.yaml @@ -3,6 +3,7 @@ on: push: branches: - main + - feat/add-helm-chart workflow_dispatch: permissions: From 5d834293df422a844cd90b7842c55bf6fd8d7890 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:14:22 -0500 Subject: [PATCH 15/19] fix: hardcode chart name Signed-off-by: jcstryker --- .github/workflows/chart-publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/chart-publish.yaml b/.github/workflows/chart-publish.yaml index a8cf25b9..d5233340 100644 --- a/.github/workflows/chart-publish.yaml +++ b/.github/workflows/chart-publish.yaml @@ -32,4 +32,4 @@ jobs: - name: Push Helm chart to GHCR OCI repository run: | - helm push ./kubernetes/headplane oci://ghcr.io/${{ github.repository }}/charts/${{ github.repository }} + helm push ./kubernetes/headplane oci://ghcr.io/${{ github.repository }}/charts/headplane From ca4e435b5adf233642516c375c9644defc27481a Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:18:10 -0500 Subject: [PATCH 16/19] test: chart releaser Signed-off-by: jcstryker --- .github/workflows/chart-publish.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/chart-publish.yaml b/.github/workflows/chart-publish.yaml index d5233340..9cbf255f 100644 --- a/.github/workflows/chart-publish.yaml +++ b/.github/workflows/chart-publish.yaml @@ -23,6 +23,11 @@ jobs: - name: Lint Helm chart run: helm lint ./kubernetes/headplane + - name: Run chart-releaser + uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + - name: Login to GHCR uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef with: From 0db44028080424410592d3aa4d9e3947173e0b69 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:23:10 -0500 Subject: [PATCH 17/19] fix: chart releaser Signed-off-by: jcstryker --- .github/workflows/chart-publish.yaml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/chart-publish.yaml b/.github/workflows/chart-publish.yaml index 9cbf255f..807fa9b4 100644 --- a/.github/workflows/chart-publish.yaml +++ b/.github/workflows/chart-publish.yaml @@ -18,13 +18,15 @@ jobs: uses: actions/checkout@v4 - name: Set up Helm - uses: azure/setup-helm@v3 + uses: azure/setup-helm@v4 - name: Lint Helm chart run: helm lint ./kubernetes/headplane - name: Run chart-releaser - uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 + uses: helm/chart-releaser-action@v1.6.0 + with: + charts_dir: kubernetes env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" @@ -35,6 +37,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Push Helm chart to GHCR OCI repository + - name: Push chart to GHCR run: | - helm push ./kubernetes/headplane oci://ghcr.io/${{ github.repository }}/charts/headplane + shopt -s nullglob + for pkg in .cr-release-packages/*.tgz; do + if [ -z "${pkg:-}" ]; then + break + fi + helm push "${pkg}" oci://ghcr.io/${{ github.repository }}/charts + done From 27a1688967af5c23989a41ac62ab5c11bef76ee2 Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:24:53 -0500 Subject: [PATCH 18/19] fix: weird bash error Signed-off-by: jcstryker --- .github/workflows/chart-publish.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/chart-publish.yaml b/.github/workflows/chart-publish.yaml index 807fa9b4..54ab0dbb 100644 --- a/.github/workflows/chart-publish.yaml +++ b/.github/workflows/chart-publish.yaml @@ -39,7 +39,6 @@ jobs: - name: Push chart to GHCR run: | - shopt -s nullglob for pkg in .cr-release-packages/*.tgz; do if [ -z "${pkg:-}" ]; then break From 3c5cea00e89700fe30cd6b149f6c09f02999827c Mon Sep 17 00:00:00 2001 From: jcstryker Date: Sun, 18 Jan 2026 16:32:34 -0500 Subject: [PATCH 19/19] chore: move chart publish workflow to separate pr Signed-off-by: jcstryker --- .github/workflows/chart-publish.yaml | 47 ---------------------------- 1 file changed, 47 deletions(-) delete mode 100644 .github/workflows/chart-publish.yaml diff --git a/.github/workflows/chart-publish.yaml b/.github/workflows/chart-publish.yaml deleted file mode 100644 index 54ab0dbb..00000000 --- a/.github/workflows/chart-publish.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: Chart Publish -on: - push: - branches: - - main - - feat/add-helm-chart - workflow_dispatch: - -permissions: - packages: write - contents: read - -jobs: - publish-chart: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Helm - uses: azure/setup-helm@v4 - - - name: Lint Helm chart - run: helm lint ./kubernetes/headplane - - - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.6.0 - with: - charts_dir: kubernetes - env: - CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - - - name: Login to GHCR - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Push chart to GHCR - run: | - for pkg in .cr-release-packages/*.tgz; do - if [ -z "${pkg:-}" ]; then - break - fi - helm push "${pkg}" oci://ghcr.io/${{ github.repository }}/charts - done