diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index 8356cfe..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Validate and deploy Mumble - -on: - push: - pull_request: - -env: - KUBECONFORM_VERSION: v0.7.0 - KUBECTL_VERSION: v1.36.3 - KUBERNETES_API: https://host.containers.internal:6443 - KUBERNETES_TLS_SERVER_NAME: 192.168.0.2 - NAMESPACE: default - -jobs: - validate: - name: Validate manifests - runs-on: ubuntu-latest - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Validate scripts and Kubernetes resources - run: | - sh -n install.sh - bash -n create-ci-kubeconfig.sh - docker run --rm --interactive \ - "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}" \ - -strict -summary < mumble.yaml - docker run --rm --interactive \ - "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}" \ - -strict -summary < ci-deployer.yaml - - deploy: - name: Deploy to Kubernetes - if: gitea.ref == 'refs/heads/main' - needs: validate - runs-on: ubuntu-latest - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Install kubectl - run: | - curl --fail --silent --show-error --location \ - --output "${RUNNER_TEMP}/kubectl" \ - "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" - chmod 0700 "${RUNNER_TEMP}/kubectl" - - - name: Configure Kubernetes access - env: - KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }} - run: | - test -n "${KUBE_CONFIG_BASE64}" - printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig" - chmod 0600 "${RUNNER_TEMP}/kubeconfig" - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - "${RUNNER_TEMP}/kubectl" config set-cluster cluster \ - --server="${KUBERNETES_API}" \ - --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}" - - - name: Apply and verify - run: | - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - "${RUNNER_TEMP}/kubectl" apply \ - --dry-run=server \ - --validate=false \ - --filename mumble.yaml - "${RUNNER_TEMP}/kubectl" apply \ - --validate=false \ - --filename mumble.yaml - - for attempt in {1..60}; do - IFS='|' read -r generation observed desired updated available unavailable <<< "$( - "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ - get deployment/mumble \ - --output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.availableReplicas}|{.status.unavailableReplicas}' - )" - - echo "Rollout ${attempt}/60: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${available}/${desired} available" - if [[ "${observed}" == "${generation}" \ - && "${updated}" == "${desired}" \ - && "${available}" == "${desired}" \ - && ( -z "${unavailable}" || "${unavailable}" == "0" ) ]]; then - echo "Mumble deployment successfully rolled out" - exit 0 - fi - - sleep 5 - done - - echo "Mumble rollout did not complete within five minutes" >&2 - exit 1 diff --git a/bootstrap-homelab-secrets.sh b/bootstrap-homelab-secrets.sh index eae97f0..089d77a 100755 --- a/bootstrap-homelab-secrets.sh +++ b/bootstrap-homelab-secrets.sh @@ -18,7 +18,7 @@ if [ "$(kubectl config current-context)" != kubernetes-admin@kubernetes ]; then exit 1 fi -for dependency in secret/postgresql secret/brunner-ninja-tls; do +for dependency in 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 @@ -26,6 +26,10 @@ for dependency in secret/postgresql secret/brunner-ninja-tls; do done if ! kubectl get secret "$SECRET_NAME" --namespace "$NAMESPACE" >/dev/null 2>&1; then + if kubectl get secret mumble-postgres --namespace "$NAMESPACE" >/dev/null 2>&1; then + echo "Restore $NAMESPACE/$SECRET_NAME from backup; do not replace its database password" >&2 + exit 1 + fi database_password=$(openssl rand -hex 32) superuser_password=$(openssl rand -hex 32) registration_password=$(openssl rand -hex 32) @@ -69,6 +73,16 @@ if ! kubectl get secret mumble-postgres --namespace "$NAMESPACE" >/dev/null 2>&1 echo "Created $NAMESPACE/mumble-postgres for the dedicated PostgreSQL chart." else echo "Keeping existing $NAMESPACE/mumble-postgres." + app_password=$(kubectl get secret "$SECRET_NAME" --namespace "$NAMESPACE" \ + -o go-template='{{index .data "database-password"}}') + database_password=$(kubectl get secret mumble-postgres --namespace "$NAMESPACE" \ + -o go-template='{{index .data "database-password"}}') + if [ "$app_password" != "$database_password" ]; then + unset app_password database_password + echo 'Mumble and PostgreSQL database passwords differ; restore from backup' >&2 + exit 1 + fi + unset app_password database_password fi echo "Secrets are ready for the Argo CD-managed Mumble release." diff --git a/ci-deployer.yaml b/ci-deployer.yaml deleted file mode 100644 index 3813020..0000000 --- a/ci-deployer.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# Bootstrap this file once with an administrator identity. CI can update only -# the two existing Mumble resources and cannot call the Kubernetes Secrets API. -apiVersion: v1 -kind: ServiceAccount -metadata: - name: mumble-deployer - namespace: default -automountServiceAccountToken: false ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: mumble-deployer - namespace: default -rules: - - apiGroups: - - "" - resources: - - services - resourceNames: - - mumble - verbs: - - get - - patch - - update - - apiGroups: - - apps - resources: - - deployments - resourceNames: - - mumble - verbs: - - get - - patch - - update ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: mumble-deployer - namespace: default -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: mumble-deployer -subjects: - - kind: ServiceAccount - name: mumble-deployer - namespace: default ---- -apiVersion: v1 -kind: Secret -metadata: - name: mumble-deployer-token - namespace: default - annotations: - kubernetes.io/service-account.name: mumble-deployer -type: kubernetes.io/service-account-token diff --git a/create-ci-kubeconfig.sh b/create-ci-kubeconfig.sh deleted file mode 100755 index e4b41f0..0000000 --- a/create-ci-kubeconfig.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -namespace=${NAMESPACE:-default} -service_account=${SERVICE_ACCOUNT:-mumble-deployer} -secret=${TOKEN_SECRET:-mumble-deployer-token} - -kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2 - -for attempt in {1..30}; do - if kubectl --namespace "${namespace}" get secret "${secret}" \ - --output=jsonpath='{.data.token}' 2>/dev/null | grep -q .; then - break - fi - if [[ "${attempt}" == 30 ]]; then - echo "Timed out waiting for the service-account token" >&2 - exit 1 - fi - sleep 1 -done - -workdir=$(mktemp --directory) -trap 'rm -rf "${workdir}"' EXIT - -server=$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}') -kubectl --namespace "${namespace}" get secret "${secret}" \ - --output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt" -token=$(kubectl --namespace "${namespace}" get secret "${secret}" \ - --output=jsonpath='{.data.token}' | base64 --decode) - -export KUBECONFIG="${workdir}/config" -kubectl config set-cluster cluster \ - --server="${server}" \ - --certificate-authority="${workdir}/ca.crt" \ - --embed-certs=true >/dev/null -kubectl config set-credentials "${service_account}" --token="${token}" >/dev/null -kubectl config set-context mumble \ - --cluster=cluster \ - --user="${service_account}" \ - --namespace="${namespace}" >/dev/null -kubectl config use-context mumble >/dev/null - -echo "Store the following single line as the Gitea Actions secret KUBE_CONFIG_BASE64." >&2 -echo "Treat it as a password; do not commit it." >&2 -base64 --wrap=0 "${KUBECONFIG}" -printf '\n' diff --git a/mumble.yaml b/mumble.yaml deleted file mode 100644 index b260af3..0000000 --- a/mumble.yaml +++ /dev/null @@ -1,298 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: mumble - namespace: default - labels: - app.kubernetes.io/name: mumble - annotations: - metallb.io/address-pool: default - mikrotik.brunner.ninja/router: newgw - mikrotik.brunner.ninja/forwardings: >- - [{"name":"tcp","servicePort":"mumble-tcp","dstAddressList":"WANs"},{"name":"udp","servicePort":"mumble-udp","dstAddressList":"WANs"}] -spec: - type: LoadBalancer - loadBalancerIP: 192.168.0.14 - externalTrafficPolicy: Local - 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 - namespace: default - 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: docker.io/library/busybox:1.38.0 - imagePullPolicy: IfNotPresent - command: - - /bin/sh - - -ec - - | - umask 077 - cat > /config/mumble-server.ini <- - 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: - requests: - cpu: 25m - memory: 64Mi - limits: - cpu: 500m - memory: 512Mi - - name: certificate-reloader - image: docker.io/library/busybox:1.38.0 - 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: brunner-ninja-tls - defaultMode: 0440