diff --git a/scripts/nextcloud-mail-sync.sh b/scripts/nextcloud-mail-sync.sh new file mode 120000 index 0000000..0f33a4e --- /dev/null +++ b/scripts/nextcloud-mail-sync.sh @@ -0,0 +1 @@ +../services/nextcloud/scripts/nextcloud-mail-sync.sh \ No newline at end of file diff --git a/scripts/nextcloud-maintenance.sh b/scripts/nextcloud-maintenance.sh new file mode 120000 index 0000000..e6b1223 --- /dev/null +++ b/scripts/nextcloud-maintenance.sh @@ -0,0 +1 @@ +../services/nextcloud/scripts/nextcloud-maintenance.sh \ No newline at end of file diff --git a/services/nextcloud/configmap.yaml b/services/nextcloud/configmap.yaml new file mode 100644 index 0000000..a6e917c --- /dev/null +++ b/services/nextcloud/configmap.yaml @@ -0,0 +1,48 @@ +# services/nextcloud/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: nextcloud-config + namespace: nextcloud +data: + extra.config.php: | + + array ( + 0 => 'cloud.bstein.dev', + ), + 'overwritehost' => 'cloud.bstein.dev', + 'overwriteprotocol' => 'https', + 'overwrite.cli.url' => 'https://cloud.bstein.dev', + 'default_phone_region' => 'US', + 'mail_smtpmode' => 'smtp', + 'mail_sendmailmode' => 'smtp', + 'mail_smtphost' => 'mail.bstein.dev', + 'mail_smtpport' => '587', + 'mail_smtpsecure' => 'tls', + 'mail_smtpauth' => true, + 'mail_smtpauthtype' => 'LOGIN', + 'mail_domain' => 'bstein.dev', + 'mail_from_address' => 'no-reply', + 'oidc_login_provider_url' => 'https://sso.bstein.dev/realms/atlas', + 'oidc_login_client_id' => getenv('OIDC_CLIENT_ID'), + 'oidc_login_client_secret' => getenv('OIDC_CLIENT_SECRET'), + 'oidc_login_auto_redirect' => false, + 'oidc_login_end_session_redirect' => true, + 'oidc_login_button_text' => 'Login with Keycloak', + 'oidc_login_hide_password_form' => false, + 'oidc_login_attributes' => + array ( + 'id' => 'preferred_username', + 'mail' => 'email', + 'name' => 'name', + ), + 'oidc_login_scope' => 'openid profile email', + 'oidc_login_unique_id' => 'preferred_username', + 'oidc_login_use_pkce' => true, + 'oidc_login_disable_registration' => false, + 'oidc_login_create_groups' => false, + # External storage for user data should be configured to Asteria via the External Storage app (admin UI), + # keeping the astreae PVC for app internals only. + ); diff --git a/services/nextcloud/cronjob.yaml b/services/nextcloud/cronjob.yaml new file mode 100644 index 0000000..86c55e1 --- /dev/null +++ b/services/nextcloud/cronjob.yaml @@ -0,0 +1,32 @@ +# services/nextcloud/cronjob.yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: nextcloud-cron + namespace: nextcloud +spec: + schedule: "*/5 * * * *" + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + securityContext: + runAsUser: 33 + runAsGroup: 33 + fsGroup: 33 + restartPolicy: OnFailure + containers: + - name: nextcloud-cron + image: nextcloud:29-apache + imagePullPolicy: IfNotPresent + command: ["/bin/sh", "-c"] + args: + - "cd /var/www/html && php -f cron.php" + volumeMounts: + - name: nextcloud-data + mountPath: /var/www/html + volumes: + - name: nextcloud-data + persistentVolumeClaim: + claimName: nextcloud-data diff --git a/services/nextcloud/deployment.yaml b/services/nextcloud/deployment.yaml new file mode 100644 index 0000000..b2c590f --- /dev/null +++ b/services/nextcloud/deployment.yaml @@ -0,0 +1,143 @@ +# services/nextcloud/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nextcloud + namespace: nextcloud + labels: + app: nextcloud +spec: + replicas: 1 + selector: + matchLabels: + app: nextcloud + template: + metadata: + labels: + app: nextcloud + spec: + nodeSelector: + hardware: rpi5 + securityContext: + fsGroup: 33 + runAsUser: 33 + runAsGroup: 33 + initContainers: + - name: fix-perms + image: alpine:3.20 + command: ["/bin/sh", "-c"] + args: + - | + chown -R 33:33 /var/www/html/config || true + chown -R 33:33 /var/www/html/data || true + securityContext: + runAsUser: 0 + runAsGroup: 0 + volumeMounts: + - name: nextcloud-data + mountPath: /var/www/html + - name: nextcloud-config + mountPath: /var/www/html/config/extra.config.php + subPath: extra.config.php + containers: + - name: nextcloud + image: nextcloud:29-apache + imagePullPolicy: IfNotPresent + env: + # DB (external secret required: nextcloud-db with keys username,password,database) + - name: POSTGRES_HOST + value: postgres-service.postgres.svc.cluster.local + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: nextcloud-db + key: database + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: nextcloud-db + key: db-username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: nextcloud-db + key: db-password + # Admin bootstrap (external secret: nextcloud-admin with keys admin-user, admin-password) + - name: NEXTCLOUD_ADMIN_USER + valueFrom: + secretKeyRef: + name: nextcloud-admin + key: admin-user + - name: NEXTCLOUD_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: nextcloud-admin + key: admin-password + - name: NEXTCLOUD_TRUSTED_DOMAINS + value: cloud.bstein.dev + - name: OVERWRITEHOST + value: cloud.bstein.dev + - name: OVERWRITEPROTOCOL + value: https + - name: OVERWRITECLIURL + value: https://cloud.bstein.dev + # SMTP (external secret: nextcloud-smtp with keys username, password) + - name: SMTP_HOST + value: mail.bstein.dev + - name: SMTP_PORT + value: "587" + - name: SMTP_SECURE + value: tls + - name: SMTP_NAME + valueFrom: + secretKeyRef: + name: nextcloud-smtp + key: smtp-username + - name: SMTP_PASSWORD + valueFrom: + secretKeyRef: + name: nextcloud-smtp + key: smtp-password + - name: MAIL_FROM_ADDRESS + value: no-reply + - name: MAIL_DOMAIN + value: bstein.dev + # OIDC (external secret: nextcloud-oidc with keys client-id, client-secret) + - name: OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: nextcloud-oidc + key: client-id + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: nextcloud-oidc + key: client-secret + - name: NEXTCLOUD_UPDATE + value: "1" + - name: APP_INSTALL + value: "mail,oidc_login,external" + ports: + - containerPort: 80 + name: http + volumeMounts: + - name: nextcloud-data + mountPath: /var/www/html + - name: nextcloud-config + mountPath: /var/www/html/config/extra.config.php + subPath: extra.config.php + resources: + requests: + cpu: 250m + memory: 1Gi + limits: + cpu: 1 + memory: 3Gi + volumes: + - name: nextcloud-data + persistentVolumeClaim: + claimName: nextcloud-data + - name: nextcloud-config + configMap: + name: nextcloud-config + defaultMode: 0444 diff --git a/services/nextcloud/ingress.yaml b/services/nextcloud/ingress.yaml new file mode 100644 index 0000000..1c60282 --- /dev/null +++ b/services/nextcloud/ingress.yaml @@ -0,0 +1,25 @@ +# services/nextcloud/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nextcloud + namespace: nextcloud + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + tls: + - hosts: + - cloud.bstein.dev + secretName: nextcloud-tls + rules: + - host: cloud.bstein.dev + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nextcloud + port: + number: 80 diff --git a/services/nextcloud/kustomization.yaml b/services/nextcloud/kustomization.yaml new file mode 100644 index 0000000..ea269cd --- /dev/null +++ b/services/nextcloud/kustomization.yaml @@ -0,0 +1,25 @@ +# services/nextcloud/kustomization.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: nextcloud +resources: + - namespace.yaml + - configmap.yaml + - pvc.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + - cronjob.yaml + - mail-sync-cronjob.yaml + - maintenance-cronjob.yaml +configMapGenerator: + - name: nextcloud-maintenance-script + files: + - maintenance.sh=scripts/nextcloud-maintenance.sh + options: + disableNameSuffixHash: true + - name: nextcloud-mail-sync-script + files: + - sync.sh=scripts/nextcloud-mail-sync.sh + options: + disableNameSuffixHash: true diff --git a/services/nextcloud/mail-sync-cronjob.yaml b/services/nextcloud/mail-sync-cronjob.yaml new file mode 100644 index 0000000..52dc3ea --- /dev/null +++ b/services/nextcloud/mail-sync-cronjob.yaml @@ -0,0 +1,58 @@ +# services/nextcloud/mail-sync-cronjob.yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: nextcloud-mail-sync + namespace: nextcloud +spec: + schedule: "0 5 * * *" + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + securityContext: + runAsUser: 0 + runAsGroup: 0 + containers: + - name: mail-sync + image: nextcloud:29-apache + imagePullPolicy: IfNotPresent + command: ["/bin/bash", "/sync/sync.sh"] + env: + - name: KC_BASE + value: https://sso.bstein.dev + - name: KC_REALM + value: atlas + - name: KC_ADMIN_USER + valueFrom: + secretKeyRef: + name: nextcloud-keycloak-admin + key: username + - name: KC_ADMIN_PASS + valueFrom: + secretKeyRef: + name: nextcloud-keycloak-admin + key: password + volumeMounts: + - name: nextcloud-data + mountPath: /var/www/html + - name: sync-script + mountPath: /sync/sync.sh + subPath: sync.sh + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumes: + - name: nextcloud-data + persistentVolumeClaim: + claimName: nextcloud-data + - name: sync-script + configMap: + name: nextcloud-mail-sync-script + defaultMode: 0755 diff --git a/services/nextcloud/maintenance-cronjob.yaml b/services/nextcloud/maintenance-cronjob.yaml new file mode 100644 index 0000000..55fcbd1 --- /dev/null +++ b/services/nextcloud/maintenance-cronjob.yaml @@ -0,0 +1,56 @@ +# services/nextcloud/maintenance-cronjob.yaml +apiVersion: batch/v1 +kind: CronJob +metadata: + name: nextcloud-maintenance + namespace: nextcloud +spec: + schedule: "30 4 * * *" + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + securityContext: + runAsUser: 0 + runAsGroup: 0 + containers: + - name: maintenance + image: nextcloud:29-apache + imagePullPolicy: IfNotPresent + command: ["/bin/bash", "/maintenance/maintenance.sh"] + env: + - name: NC_URL + value: https://cloud.bstein.dev + - name: ADMIN_USER + valueFrom: + secretKeyRef: + name: nextcloud-admin + key: admin-user + - name: ADMIN_PASS + valueFrom: + secretKeyRef: + name: nextcloud-admin + key: admin-password + volumeMounts: + - name: nextcloud-data + mountPath: /var/www/html + - name: maintenance-script + mountPath: /maintenance/maintenance.sh + subPath: maintenance.sh + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumes: + - name: nextcloud-data + persistentVolumeClaim: + claimName: nextcloud-data + - name: maintenance-script + configMap: + name: nextcloud-maintenance-script + defaultMode: 0755 diff --git a/services/nextcloud/namespace.yaml b/services/nextcloud/namespace.yaml new file mode 100644 index 0000000..fe63672 --- /dev/null +++ b/services/nextcloud/namespace.yaml @@ -0,0 +1,5 @@ +# services/nextcloud/namespace.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: nextcloud diff --git a/services/nextcloud/pvc.yaml b/services/nextcloud/pvc.yaml new file mode 100644 index 0000000..dd929b6 --- /dev/null +++ b/services/nextcloud/pvc.yaml @@ -0,0 +1,13 @@ +# services/nextcloud/pvc.yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nextcloud-data + namespace: nextcloud +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 200Gi + storageClassName: astreae diff --git a/services/nextcloud/scripts/nextcloud-mail-sync.sh b/services/nextcloud/scripts/nextcloud-mail-sync.sh new file mode 100755 index 0000000..6b0adb1 --- /dev/null +++ b/services/nextcloud/scripts/nextcloud-mail-sync.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -euo pipefail + +KC_BASE="${KC_BASE:?}" +KC_REALM="${KC_REALM:?}" +KC_ADMIN_USER="${KC_ADMIN_USER:?}" +KC_ADMIN_PASS="${KC_ADMIN_PASS:?}" + +if ! command -v jq >/dev/null 2>&1; then + apt-get update && apt-get install -y jq curl >/dev/null +fi + +token=$( + curl -s -d "grant_type=password" \ + -d "client_id=admin-cli" \ + -d "username=${KC_ADMIN_USER}" \ + -d "password=${KC_ADMIN_PASS}" \ + "${KC_BASE}/realms/master/protocol/openid-connect/token" | jq -r '.access_token' +) + +if [[ -z "${token}" || "${token}" == "null" ]]; then + echo "Failed to obtain admin token" + exit 1 +fi + +users=$(curl -s -H "Authorization: Bearer ${token}" \ + "${KC_BASE}/admin/realms/${KC_REALM}/users?max=2000") + +echo "${users}" | jq -c '.[]' | while read -r user; do + username=$(echo "${user}" | jq -r '.username') + email=$(echo "${user}" | jq -r '.email // empty') + app_pw=$(echo "${user}" | jq -r '.attributes.mailu_app_password[0] // empty') + [[ -z "${email}" || -z "${app_pw}" ]] && continue + echo "Syncing ${email}" + runuser -u www-data -- php occ mail:account:create \ + "${username}" "${username}" "${email}" \ + mail.bstein.dev 993 ssl "${email}" "${app_pw}" \ + mail.bstein.dev 587 tls "${email}" "${app_pw}" login || true +done diff --git a/services/nextcloud/scripts/nextcloud-maintenance.sh b/services/nextcloud/scripts/nextcloud-maintenance.sh new file mode 100755 index 0000000..e8ea18c --- /dev/null +++ b/services/nextcloud/scripts/nextcloud-maintenance.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -euo pipefail + +NC_URL="${NC_URL:-https://cloud.bstein.dev}" +ADMIN_USER="${ADMIN_USER:?}" +ADMIN_PASS="${ADMIN_PASS:?}" + +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq curl jq >/dev/null + +run_occ() { + runuser -u www-data -- php occ "$@" +} + +log() { echo "[$(date -Is)] $*"; } + +log "Applying Atlas theming" +run_occ theming:config name "Atlas Cloud" +run_occ theming:config slogan "Unified access to Atlas services" +run_occ theming:config url "https://cloud.bstein.dev" +run_occ theming:config color "#0f172a" +run_occ theming:config disable-user-theming yes + +log "Setting default quota to 200 GB" +run_occ config:app:set files default_quota --value "200 GB" + +API_BASE="${NC_URL}/ocs/v2.php/apps/external/api/v1" +AUTH=(-u "${ADMIN_USER}:${ADMIN_PASS}" -H "OCS-APIRequest: true") + +log "Removing existing external links" +existing=$(curl -sf "${AUTH[@]}" "${API_BASE}?format=json" | jq -r '.ocs.data[].id // empty') +for id in ${existing}; do + curl -sf "${AUTH[@]}" -X DELETE "${API_BASE}/sites/${id}?format=json" >/dev/null || true +done + +SITES=( + "Vaultwarden|https://vault.bstein.dev" + "Jellyfin|https://stream.bstein.dev" + "Gitea|https://scm.bstein.dev" + "Jenkins|https://ci.bstein.dev" + "Zot|https://registry.bstein.dev" + "Vault|https://secret.bstein.dev" + "Jitsi|https://meet.bstein.dev" + "Grafana|https://metrics.bstein.dev" + "Chat LLM|https://chat.ai.bstein.dev" + "Vision|https://draw.ai.bstein.dev" + "STT/TTS|https://talk.ai.bstein.dev" +) + +log "Seeding external links" +for entry in "${SITES[@]}"; do + IFS="|" read -r name url <<<"${entry}" + curl -sf "${AUTH[@]}" -X POST "${API_BASE}/sites?format=json" \ + -d "name=${name}" \ + -d "url=${url}" \ + -d "lang=" \ + -d "type=link" \ + -d "device=" \ + -d "icon=" \ + -d "groups[]=" \ + -d "redirect=1" >/dev/null +done + +log "Maintenance run completed" diff --git a/services/nextcloud/service.yaml b/services/nextcloud/service.yaml new file mode 100644 index 0000000..ab160fb --- /dev/null +++ b/services/nextcloud/service.yaml @@ -0,0 +1,13 @@ +# services/nextcloud/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: nextcloud + namespace: nextcloud +spec: + selector: + app: nextcloud + ports: + - name: http + port: 80 + targetPort: http