Add versioned Mumble chart with optional PostgreSQL
Validate, publish and promote Mumble chart / chart (push) Successful in 33s
Validate and deploy Mumble / Deploy to Kubernetes (push) Successful in 21s
Validate and deploy Mumble / Validate manifests (push) Successful in 10s

This commit is contained in:
feedc0de committed 2026-10-19 21:44:23 +02:00
1 parent 81e0ce2da2
commit cc39c5e9c2
14 files changed
+587 -255

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
name: Validate, publish and promote Mumble chart
on:
push:
pull_request:
jobs:
chart:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: azure/setup-helm@v4
with:
version: v4.3.0
- name: Validate chart
run: |
python3 -m venv "${RUNNER_TEMP}/chart-test"
"${RUNNER_TEMP}/chart-test/bin/pip" install PyYAML
PATH="${RUNNER_TEMP}/chart-test/bin:${PATH}" ./test.sh
- name: Select immutable chart version
if: gitea.ref == 'refs/heads/main'
run: printf 'CHART_VERSION=%s\n' "$(./scripts/chart-version.sh)" >> "${GITHUB_ENV}"
- name: Package and publish chart
if: gitea.ref == 'refs/heads/main'
env:
PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "${PACKAGE_USERNAME}" && test -n "${PACKAGE_TOKEN}"
mkdir -p .chart-packages
helm package chart/mumble --destination .chart-packages --version "${CHART_VERSION}"
curl --fail --silent --show-error --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \
--request POST --upload-file ".chart-packages/mumble-${CHART_VERSION}.tgz" \
https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts
- name: Check current main
id: current-main
if: gitea.ref == 'refs/heads/main'
env:
COMMIT_SHA: ${{ gitea.sha }}
run: |
git fetch --no-tags origin main
if test "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}"; then
echo 'promote=true' >> "${GITEA_OUTPUT}"
else
echo 'promote=false' >> "${GITEA_OUTPUT}"
fi
- uses: actions/checkout@v4
if: steps.current-main.outputs.promote == 'true'
with:
repository: feedc0de/homelab-gitops
ref: main
token: ${{ secrets.GITOPS_TOKEN }}
path: homelab-gitops
- name: Promote chart
if: steps.current-main.outputs.promote == 'true'
working-directory: homelab-gitops
run: |
set -euo pipefail
./scripts/update-chart-version.sh mumble "${CHART_VERSION}"
git config user.name feedc0de
git config user.email feedc0de@users.noreply.code.brunner.ninja
git add bootstrap/mumble.yaml
if git diff --cached --quiet; then exit 0; fi
git commit -m "Deploy Mumble chart ${CHART_VERSION}"
for attempt in 1 2 3; do
git push origin HEAD:main && exit 0
test "${attempt}" = 3 && exit 1
git fetch origin main
git rebase origin/main
done
+1
View File
@@ -5,3 +5,4 @@ mumble-server.ini
kubeconfig
*.kubeconfig
.env
chart/mumble/charts/
+10 -172
View File
@@ -1,181 +1,19 @@
# Mumble on Kubernetes
# Mumble Helm chart
Production deployment for the brunner.ninja Mumble server. It runs the
official Mumble server image with trusted TLS, a MetalLB address, and the
shared PostgreSQL instance in the `default` namespace.
## Architecture
- `mumblevoip/mumble-server:v1.5.915-1` runs as an unprivileged user.
- MetalLB advertises `192.168.0.14`; TCP and UDP use port `64738` on the same
LoadBalancer Service.
- `brunner-ninja-tls`, maintained by cert-manager, supplies the certificate for
both `brunner.ninja` and `*.brunner.ninja`.
- A small sidecar watches the mounted certificate and asks Mumble to reload TLS
with `SIGUSR1` after renewal.
- Mumble stores channels, ACLs, registered users, bans, and server state in the
`mumble` database on `postgresql.default.svc.cluster.local`.
- `/data` and the generated INI are ephemeral. No Mumble PVC is needed.
- The single replica uses a `Recreate` strategy because two processes must not
serve the same Mumble virtual server database concurrently.
The runtime INI is rendered into a mode-`0600` in-memory volume. This avoids an
upstream container-entrypoint behavior that can print `registerPassword` when
configuration is supplied through environment variables.
## Repository contents
- `mumble.yaml`: LoadBalancer Service and Mumble Deployment.
- `install.sh`: administrator installation, credential generation, idempotent
PostgreSQL bootstrap, and rollout verification.
- `test.sh`: local script and offline Kubernetes schema validation.
- `ci-deployer.yaml`: restricted Gitea Actions ServiceAccount, Role, binding,
and revocable token request.
- `create-ci-kubeconfig.sh`: one-time CI identity bootstrap.
- `.gitea/workflows/deploy.yml`: validation on every push and pull request;
deployment after successful validation of `main`.
The old host-level `mumble-server.ini` is intentionally ignored because it
contains obsolete plaintext credentials. Do not commit kubeconfigs, database
passwords, TLS keys, or generated Kubernetes Secret values.
## Prerequisites
- Kubernetes and kubectl.
- MetalLB pool `default` containing the reserved address `192.168.0.14`.
- `default/postgresql` Service and Secret. The Secret must contain
`postgres-password`.
- `default/brunner-ninja-tls`, maintained by the existing cert-manager
`letsencrypt-dns` setup.
- Router forwarding of TCP and UDP `64738` to `192.168.0.14`. On the MikroTik,
the two rules use `dst-address-list=WANs`, which also permits LAN hairpin
connections through `brunner.ninja`.
## Install or restore manually
Run the first installation with an administrator kubeconfig:
This repository maintains a portable Helm chart for the official `mumblevoip/mumble-server` image. It declares Mumble's Deployment, TCP/UDP Service, private runtime config rendering, TLS reload sidecar, probes, and optional Bitnami PostgreSQL dependency. It publishes to the public chart repository:
```sh
./install.sh
helm repo add brunner.ninja https://brunner.ninja/charts
helm repo update brunner.ninja
helm show values brunner.ninja/mumble
```
On the first run, the script creates `default/mumble-secrets` with independent
256-bit random database, SuperUser, and public-registry passwords. It then
creates the PostgreSQL role and database if absent. Later runs preserve the
Secret and database, reset the role to the stored password, reapply ownership
and grants, and safely recreate or update the workload.
For a separate cluster, create a TLS Secret, a `mumble-secrets` Secret containing `database-password`, `superuser-password`, and `registration-password`, and a PostgreSQL credential Secret if enabling the subchart. Put Secret *names*, database host, domain/registration details, and networking policy in a values file. See `values.example.yaml`. `./install.sh VALUES_FILE` performs a local Helm upgrade/install with `--wait`; `./test.sh` validates the chart. Do not run `install.sh` against the brunner.ninja cluster after Argo CD adoption.
Rotate only the public Mumble registry password with:
The PostgreSQL subchart is optional. With `postgresql.enabled=true`, set `postgresql.auth.existingSecret` to a stable Secret with separate PostgreSQL administrator and Mumble database passwords; configure `postgresql.auth.secretKeys` to identify their keys. The same Mumble database password must be available to the application via `mumble-secrets/database-password`. This chart does not render plaintext credentials. Set `postgresql.primary.persistence.*` for durable storage and a retention policy. With the subchart disabled, Mumble's init container bootstraps its role and database on an existing PostgreSQL server using the separate administrator Secret.
```sh
ROTATE_REGISTRATION_PASSWORD=true ./install.sh
```
The homelab deployment is selected in [homelab-gitops](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/mumble). It uses the optional PostgreSQL subchart and keeps the existing `default/mumble` Service and Deployment names, the `mumble-secrets` and TLS Secrets, and TCP/UDP port 64738. The old shared PostgreSQL database is deliberately left intact as rollback data. `bootstrap-homelab-secrets.sh` is a guarded, one-time secret recovery/bootstrap helper; it does not deploy workloads. Back up the dedicated PostgreSQL PVC and both Mumble Secrets together.
Retrieve the Mumble administrator password without placing it in Git:
Chart package versions come from Git tags using `git describe --tags --long`. The tag `v1.5.939` denotes the tested upstream Mumble release; a later chart-only commit publishes a distinct `1.5.939-N-gHASH` chart version. Source CI validates the chart, publishes it to the indexed `brunner.ninja` repository, and promotes only this chart's selected version in GitOps. CI has no Kubernetes credential.
```sh
kubectl get secret mumble-secrets -n default \
-o jsonpath='{.data.superuser-password}' | base64 -d; echo
```
Connect to `brunner.ninja:64738`.
### Restoring a PostgreSQL backup
Stop Mumble before replacing its database:
```sh
kubectl delete deployment mumble -n default
```
Restore the `mumble` database and ensure its objects are owned by role
`mumble`, then run `./install.sh`. The bootstrap is idempotent: it does not drop
an existing database or tables. Mumble may migrate an older compatible schema
when it starts. Its channels and registrations remain in PostgreSQL.
## Validate locally
```sh
./test.sh
```
The test checks both shell scripts and validates the Kubernetes resources with
kubeconform without contacting the cluster. It uses a locally installed
kubeconform binary when available, otherwise the official
`ghcr.io/yannh/kubeconform:v0.7.0` container through Docker.
## Automatic deployment with Gitea Actions
The workflow validates every push and pull request. A successful push to
`main` applies `mumble.yaml` and polls the Deployment for up to five minutes.
It does not build a container because this repository deliberately uses the
official, version-pinned Mumble image.
CI does not invoke `install.sh` and cannot create application credentials or
bootstrap PostgreSQL. Its Role can call `get`, `patch`, and `update` only for
the existing `default/mumble` Service and Deployment. It cannot create or
delete workloads and has no direct Kubernetes Secrets API permission.
### One-time CI setup
1. Install Mumble manually with `./install.sh` so the Service, Deployment,
application Secret, role, and database exist.
2. With an administrator kubeconfig active, run:
```sh
./create-ci-kubeconfig.sh
```
3. Copy the single base64 output line into the Gitea repository Actions secret
named `KUBE_CONFIG_BASE64`.
4. Protect `main` and require the validation job before merging.
5. Push a harmless README change and confirm the deploy job reports a healthy
rollout.
The runner networking in the workflow matches the existing brunner.ninja
runner: API URL `https://host.containers.internal:6443`, with TLS server name
`192.168.0.2`.
To revoke CI access, delete the token and remove the Gitea secret:
```sh
kubectl delete secret mumble-deployer-token -n default
```
Running `./create-ci-kubeconfig.sh` again recreates the token and prints a new
kubeconfig.
### Security boundary
The Role prevents ordinary API reads of Secrets and limits updates to two named
objects. Kubernetes RBAC cannot restrict individual fields inside a Deployment
patch, however. A malicious change merged into `main` could modify the Mumble
pod specification and potentially expose Secrets already referenced by that
workload. Branch protection and review of manifest and workflow changes remain
part of the security boundary. Admission policy would be required to constrain
specific pod fields further.
## Gitea CI or Argo CD?
Gitea CI is the recommended deployment mechanism for this repository today.
It is already proven on this cluster, the deployment is only two resources,
and it does not automatically undo deliberate maintenance such as deleting the
Deployment while restoring PostgreSQL.
| Capability | Gitea Actions used here | Argo CD |
| --- | --- | --- |
| Trigger | Push to `main` | Continuous Git reconciliation/webhook |
| Drift detection | No | Built-in diff and health view |
| Self-healing | No; rerun CI or `install.sh` | Optional automatic reconciliation |
| Deleted Deployment | Stays deleted for maintenance | Recreated when self-heal is enabled |
| Cluster credential | Restricted token per repository | Repository credential plus central Argo controller |
| Rollout view | Action log | Application resource tree, health, sync history, and UI |
| Pruning | Not performed | Optional, with confirmation controls available |
| Secret handling | Existing Secrets remain out of Git | Still needs an external Secret strategy |
Argo CD becomes attractive when several more workloads should share one
consistent deployment model, when live drift must be visible or automatically
repaired, or when multiple clusters/environments are introduced. It can remove
per-repository Kubernetes deploy tokens because CI only needs to update Git.
Adoption should start with an `AppProject`, explicit namespace/resource limits,
and a maintenance procedure that disables sync before database restores.
The old `mumble-server.ini` file is ignored because it contains obsolete plaintext credentials. Never commit it, kubeconfigs, generated Secret values, or database dumps.
+74
View File
@@ -0,0 +1,74 @@
#!/bin/sh
set -eu
umask 077
NAMESPACE=default
SECRET_NAME=mumble-secrets
for command in kubectl openssl; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Required command not found: $command" >&2
exit 1
fi
done
if [ "$(kubectl config current-context)" != kubernetes-admin@kubernetes ]; then
echo 'Refusing to deploy outside the homelab context' >&2
exit 1
fi
for dependency in secret/postgresql secret/brunner-ninja-tls; do
if ! kubectl get "$dependency" --namespace "$NAMESPACE" >/dev/null 2>&1; then
echo "Required Kubernetes object not found: $NAMESPACE/$dependency" >&2
exit 1
fi
done
if ! kubectl get secret "$SECRET_NAME" --namespace "$NAMESPACE" >/dev/null 2>&1; then
database_password=$(openssl rand -hex 32)
superuser_password=$(openssl rand -hex 32)
registration_password=$(openssl rand -hex 32)
kubectl create secret generic "$SECRET_NAME" \
--namespace "$NAMESPACE" \
--from-literal="database-password=$database_password" \
--from-literal="superuser-password=$superuser_password" \
--from-literal="registration-password=$registration_password"
unset database_password superuser_password registration_password
echo "Created $NAMESPACE/$SECRET_NAME with random database, SuperUser, and public-registry passwords."
else
echo "Keeping existing $NAMESPACE/$SECRET_NAME."
registration_password_present=$(kubectl get secret "$SECRET_NAME" \
--namespace "$NAMESPACE" \
-o go-template='{{if index .data "registration-password"}}yes{{end}}')
if [ -z "$registration_password_present" ] || [ "${ROTATE_REGISTRATION_PASSWORD:-false}" = true ]; then
registration_password=$(openssl rand -hex 32)
kubectl patch secret "$SECRET_NAME" \
--namespace "$NAMESPACE" \
--type=merge \
--patch "{\"stringData\":{\"registration-password\":\"$registration_password\"}}" >/dev/null
unset registration_password
echo "Set a new random public-registry password in $NAMESPACE/$SECRET_NAME."
fi
unset registration_password_present
fi
if ! kubectl get secret mumble-postgres --namespace "$NAMESPACE" >/dev/null 2>&1; then
admin_password=$(openssl rand -hex 32)
database_password=$(kubectl get secret "$SECRET_NAME" --namespace "$NAMESPACE" \
-o go-template='{{index .data "database-password"}}' | base64 -d)
kubectl create secret generic mumble-postgres --namespace "$NAMESPACE" \
--from-literal="postgres-password=$admin_password" \
--from-literal="database-password=$database_password"
unset admin_password database_password
echo "Created $NAMESPACE/mumble-postgres for the dedicated PostgreSQL chart."
else
echo "Keeping existing $NAMESPACE/mumble-postgres."
fi
echo "Secrets are ready for the Argo CD-managed Mumble release."
+6
View File
@@ -0,0 +1,6 @@
dependencies:
- name: postgresql
repository: https://charts.bitnami.com/bitnami
version: 18.12.4
digest: sha256:517a59bc973bf05118c845d87617688f9cec2080e4112ec494eb4b104c00cb52
generated: "2026-10-19T21:39:30.696228885+02:00"
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v2
name: mumble
description: Mumble server with external PostgreSQL and TLS certificate reload
type: application
version: 0.0.0
appVersion: v1.5.939
dependencies:
- name: postgresql
version: 18.12.4
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
+312
View File
@@ -0,0 +1,312 @@
{{- if .Values.mumble.enabled }}
apiVersion: v1
kind: Service
metadata:
name: mumble
labels:
app.kubernetes.io/name: mumble
{{- with .Values.service.annotations }}
annotations:
{{ toYaml . | indent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "LoadBalancer" }}
loadBalancerIP: {{ .Values.service.loadBalancerIP | quote }}
{{- end }}
{{- if eq .Values.service.type "LoadBalancer" }}
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
{{- end }}
selector:
app.kubernetes.io/name: mumble
ports:
- name: mumble-tcp
port: 64738
targetPort: mumble-tcp
protocol: TCP
- name: mumble-udp
port: 64738
targetPort: mumble-udp
protocol: UDP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mumble
labels:
app.kubernetes.io/name: mumble
spec:
replicas: 1
# Two Mumble processes must not serve the same virtual server/database.
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: mumble
template:
metadata:
labels:
app.kubernetes.io/name: mumble
spec:
# Allows the certificate watcher to send SIGUSR1 to mumble-server.
shareProcessNamespace: true
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
securityContext:
fsGroup: 10000
fsGroupChangePolicy: OnRootMismatch
initContainers:
- name: render-config
image: {{ .Values.images.busybox | quote }}
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
umask 077
cat > /config/mumble-server.ini <<EOF
database=mumble
dbDriver=QPSQL
dbUsername=mumble
dbPassword=$MUMBLE_DB_PASSWORD
dbHost=$MUMBLE_DB_HOST
dbPort=5432
ice="tcp -h 127.0.0.1 -p 6502"
logfile=
welcometext="$MUMBLE_WELCOME_TEXT"
port=64738
bandwidth=558000
users=100
messageburst=5
messagelimit=1
allowping=true
registerName=$MUMBLE_REGISTRATION_NAME
registerPassword=$MUMBLE_REGISTRATION_PASSWORD
registerUrl=$MUMBLE_REGISTRATION_URL
registerHostname=$MUMBLE_REGISTRATION_HOSTNAME
registerLocation=$MUMBLE_REGISTRATION_LOCATION
sslCert=/certs/tls.crt
sslKey=/certs/tls.key
[Ice]
Ice.Warn.UnknownProperties=1
Ice.MessageSizeMax=65536
EOF
env:
- name: MUMBLE_DB_HOST
value: {{ .Values.database.host | quote }}
- name: MUMBLE_WELCOME_TEXT
value: {{ .Values.config.welcomeText | quote }}
- name: MUMBLE_REGISTRATION_NAME
value: {{ .Values.config.registrationName | quote }}
- name: MUMBLE_REGISTRATION_URL
value: {{ .Values.config.registrationURL | quote }}
- name: MUMBLE_REGISTRATION_HOSTNAME
value: {{ .Values.config.registrationHostname | quote }}
- name: MUMBLE_REGISTRATION_LOCATION
value: {{ .Values.config.registrationLocation | quote }}
- name: MUMBLE_DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.mumble | quote }}
key: database-password
- name: MUMBLE_REGISTRATION_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.mumble | quote }}
key: registration-password
volumeMounts:
- name: config
mountPath: /config
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 1m
memory: 2Mi
limits:
cpu: 20m
memory: 16Mi
{{- if not .Values.postgresql.enabled }}
- name: bootstrap-postgresql
image: {{ .Values.images.postgresqlClient | quote }}
imagePullPolicy: IfNotPresent
command:
- /bin/bash
- -ec
- |
psql \
--host="$MUMBLE_DB_HOST" \
--port=5432 \
--username=postgres \
--dbname=postgres \
--set=ON_ERROR_STOP=1 \
--set=mumble_password="$MUMBLE_DB_PASSWORD" <<'SQL'
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', 'mumble', :'mumble_password')
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'mumble')
\gexec
SELECT format('ALTER ROLE %I LOGIN PASSWORD %L', 'mumble', :'mumble_password')
\gexec
SELECT format('CREATE DATABASE %I OWNER %I', 'mumble', 'mumble')
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_database WHERE datname = 'mumble')
\gexec
ALTER DATABASE mumble OWNER TO mumble;
REVOKE ALL ON DATABASE mumble FROM PUBLIC;
GRANT ALL PRIVILEGES ON DATABASE mumble TO mumble;
SQL
env:
- name: MUMBLE_DB_HOST
value: {{ .Values.database.host | quote }}
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.postgresql | quote }}
key: postgres-password
- name: MUMBLE_DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.mumble | quote }}
key: database-password
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
{{- end }}
containers:
- name: mumble
image: {{ printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) | quote }}
imagePullPolicy: IfNotPresent
ports:
- name: mumble-tcp
containerPort: 64738
protocol: TCP
- name: mumble-udp
containerPort: 64738
protocol: UDP
env:
- name: MUMBLE_CHOWN_DATA
value: "false"
- name: MUMBLE_CUSTOM_CONFIG_FILE
value: /config/mumble-server.ini
- name: MUMBLE_SUPERUSER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.secrets.mumble | quote }}
key: superuser-password
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /config
readOnly: true
- name: certificate
mountPath: /certs
readOnly: true
readinessProbe:
exec:
command:
- /bin/bash
- -ec
- >-
awk '$2 ~ /:FCE2$/ && $4 == "0A" { found=1 }
END { exit(found ? 0 : 1) }' /proc/net/tcp /proc/net/tcp6
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
exec:
command:
- /bin/bash
- -ec
- >-
awk '$2 ~ /:FCE2$/ && $4 == "0A" { found=1 }
END { exit(found ? 0 : 1) }' /proc/net/tcp /proc/net/tcp6
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
resources:
{{ toYaml .Values.resources | indent 12 }}
- name: certificate-reloader
image: {{ .Values.images.busybox | quote }}
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
previous="$(sha256sum /certs/tls.crt /certs/tls.key)"
while sleep 60; do
current="$(sha256sum /certs/tls.crt /certs/tls.key)"
if [ "$current" != "$previous" ]; then
set -- $(pidof mumble-server)
if [ "$#" -gt 0 ]; then
kill -USR1 "$@"
echo "Reloaded Mumble TLS certificate"
fi
previous="$current"
fi
done
volumeMounts:
- name: certificate
mountPath: /certs
readOnly: true
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
runAsUser: 10000
runAsGroup: 10000
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 1m
memory: 2Mi
limits:
cpu: 20m
memory: 16Mi
volumes:
# PostgreSQL stores Mumble's durable state; /data only holds generated config.
- name: data
emptyDir: {}
- name: config
emptyDir: {}
- name: certificate
secret:
secretName: {{ .Values.secrets.tls | quote }}
defaultMode: 0440
{{- end }}
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"image": {"type": "object", "properties": {"repository": {"type": "string", "minLength": 1}, "tag": {"type": "string"}}},
"service": {"type": "object", "properties": {"type": {"enum": ["LoadBalancer", "ClusterIP"]}, "loadBalancerIP": {"type": "string"}, "externalTrafficPolicy": {"enum": ["Local", "Cluster"]}, "annotations": {"type": "object", "additionalProperties": {"type": "string"}}}},
"database": {"type": "object", "properties": {"host": {"type": "string", "minLength": 1}}},
"secrets": {"type": "object", "properties": {"mumble": {"type": "string", "minLength": 1}, "postgresql": {"type": "string", "minLength": 1}, "tls": {"type": "string", "minLength": 1}}},
"config": {"type": "object", "properties": {"welcomeText": {"type": "string"}, "registrationName": {"type": "string"}, "registrationURL": {"type": "string"}, "registrationHostname": {"type": "string"}, "registrationLocation": {"type": "string"}}},
"resources": {"type": "object"}
}
}
+37
View File
@@ -0,0 +1,37 @@
image:
repository: mumblevoip/mumble-server
tag: ""
mumble:
enabled: true
images:
busybox: docker.io/library/busybox:1.38.0
postgresqlClient: bitnamilegacy/postgresql:17.6.0-debian-12-r4
service:
type: LoadBalancer
annotations: {}
loadBalancerIP: ""
externalTrafficPolicy: Local
database:
host: postgresql
secrets:
mumble: mumble-secrets
postgresql: postgresql
tls: mumble-tls
config:
welcomeText: Welcome to Mumble
registrationName: mumble.example.org
registrationURL: https://mumble.example.org/
registrationHostname: mumble.example.org
registrationLocation: ""
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 500m
memory: 512Mi
postgresql:
enabled: false
auth:
username: mumble
database: mumble
+4 -60
View File
@@ -1,62 +1,6 @@
#!/bin/sh
set -eu
NAMESPACE=default
SECRET_NAME=mumble-secrets
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
for command in kubectl openssl; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Required command not found: $command" >&2
exit 1
fi
done
for dependency in secret/postgresql secret/brunner-ninja-tls; do
if ! kubectl get "$dependency" --namespace "$NAMESPACE" >/dev/null 2>&1; then
echo "Required Kubernetes object not found: $NAMESPACE/$dependency" >&2
exit 1
fi
done
if ! kubectl get secret "$SECRET_NAME" --namespace "$NAMESPACE" >/dev/null 2>&1; then
database_password=$(openssl rand -hex 32)
superuser_password=$(openssl rand -hex 32)
registration_password=$(openssl rand -hex 32)
kubectl create secret generic "$SECRET_NAME" \
--namespace "$NAMESPACE" \
--from-literal="database-password=$database_password" \
--from-literal="superuser-password=$superuser_password" \
--from-literal="registration-password=$registration_password"
unset database_password superuser_password registration_password
echo "Created $NAMESPACE/$SECRET_NAME with random database, SuperUser, and public-registry passwords."
else
echo "Keeping existing $NAMESPACE/$SECRET_NAME."
registration_password_present=$(kubectl get secret "$SECRET_NAME" \
--namespace "$NAMESPACE" \
-o go-template='{{if index .data "registration-password"}}yes{{end}}')
if [ -z "$registration_password_present" ] || [ "${ROTATE_REGISTRATION_PASSWORD:-false}" = true ]; then
registration_password=$(openssl rand -hex 32)
kubectl patch secret "$SECRET_NAME" \
--namespace "$NAMESPACE" \
--type=merge \
--patch "{\"stringData\":{\"registration-password\":\"$registration_password\"}}" >/dev/null
unset registration_password
echo "Set a new random public-registry password in $NAMESPACE/$SECRET_NAME."
fi
unset registration_password_present
fi
kubectl apply -f "$SCRIPT_DIR/mumble.yaml"
kubectl rollout status deployment/mumble --namespace "$NAMESPACE" --timeout=5m
echo
echo "Mumble is available at brunner.ninja:64738 (MetalLB 192.168.0.14)."
echo "Retrieve the SuperUser password with:"
echo "kubectl get secret $SECRET_NAME -n $NAMESPACE -o jsonpath='{.data.superuser-password}' | base64 -d; echo"
cd "$(dirname "$0")"
values=${1:?usage: ./install.sh VALUES_FILE}
./test.sh
helm upgrade --install mumble chart/mumble --namespace "${MUMBLE_NAMESPACE:-default}" --create-namespace --values "$values" --wait --timeout 15m
+3 -3
View File
@@ -55,7 +55,7 @@ spec:
fsGroupChangePolicy: OnRootMismatch
initContainers:
- name: render-config
image: busybox:1.37.0
image: docker.io/library/busybox:1.38.0
imagePullPolicy: IfNotPresent
command:
- /bin/sh
@@ -177,7 +177,7 @@ spec:
memory: 128Mi
containers:
- name: mumble
image: mumblevoip/mumble-server:v1.5.915-1
image: mumblevoip/mumble-server:v1.5.939
imagePullPolicy: IfNotPresent
ports:
- name: mumble-tcp
@@ -247,7 +247,7 @@ spec:
cpu: 500m
memory: 512Mi
- name: certificate-reloader
image: busybox:1.37.0
image: docker.io/library/busybox:1.38.0
imagePullPolicy: IfNotPresent
command:
- /bin/sh
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
description=$(git describe --tags --long --match 'v[0-9]*' --dirty)
case "$description" in *-dirty) echo 'Refusing to package a dirty tree' >&2; exit 1 ;; esac
version=${description#v}
case "$version" in *-0-g*) version=${version%%-0-g*} ;; esac
printf '%s\n' "$version"
+29 -20
View File
@@ -1,24 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
sh -n install.sh bootstrap-homelab-secrets.sh scripts/chart-version.sh
helm repo add bitnami https://charts.bitnami.com/bitnami --force-update >/dev/null
helm dependency build chart/mumble >/dev/null
helm lint chart/mumble
helm lint chart/mumble --set postgresql.enabled=true
python3 - <<'PY'
import subprocess
import yaml
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
def render(*extra):
output = subprocess.check_output(['helm', 'template', 'mumble', 'chart/mumble',
'--namespace', 'default', '--set', 'postgresql.enabled=true',
*extra], text=True)
return [doc for doc in yaml.safe_load_all(output) if doc]
sh -n "${project_dir}/install.sh"
bash -n "${project_dir}/create-ci-kubeconfig.sh"
if command -v kubeconform >/dev/null 2>&1; then
kubeconform -strict -summary "${project_dir}/mumble.yaml" "${project_dir}/ci-deployer.yaml"
elif command -v docker >/dev/null 2>&1; then
docker run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0 \
-strict -summary < "${project_dir}/mumble.yaml"
docker run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0 \
-strict -summary < "${project_dir}/ci-deployer.yaml"
else
echo "Required command not found: install kubeconform or Docker" >&2
exit 1
fi
echo "Mumble scripts and Kubernetes resources are valid."
staged = render('--set', 'mumble.enabled=false')
assert not any(doc['kind'] == 'Deployment' for doc in staged)
assert any(doc['kind'] == 'StatefulSet' and doc['metadata']['name'] == 'mumble-postgresql' for doc in staged)
active = render()
deployment = next(doc for doc in active if doc['kind'] == 'Deployment' and doc['metadata']['name'] == 'mumble')
service = next(doc for doc in active if doc['kind'] == 'Service' and doc['metadata']['name'] == 'mumble')
assert not service['spec'].get('loadBalancerIP')
assert {port['protocol'] for port in service['spec']['ports']} == {'TCP', 'UDP'}
assert deployment['spec']['strategy']['type'] == 'Recreate'
pod = deployment['spec']['template']['spec']
assert [container['name'] for container in pod['initContainers']] == ['render-config']
assert pod['containers'][0]['image'].endswith('v1.5.939')
assert any(volume.get('secret', {}).get('secretName') == 'mumble-tls' for volume in pod['volumes'])
print('Mumble chart renders staged PostgreSQL and active Mumble without cleartext credentials.')
PY
+10
View File
@@ -0,0 +1,10 @@
database:
host: postgresql.example.svc.cluster.local
secrets:
mumble: mumble-secrets
postgresql: postgresql
tls: mumble-tls
config:
registrationName: mumble.example.org
registrationURL: https://mumble.example.org/
registrationHostname: mumble.example.org