From b40b8a9ff9cd55e230029c777c9544b4d3de9dad Mon Sep 17 00:00:00 2001 From: Oleksandr Berezovskyi Date: Wed, 25 Feb 2026 23:57:02 +0200 Subject: [PATCH] feat(k8s/archmirror): add Arch Linux mirror stack --- .../app/archmirror/configmap-nginx.yaml | 38 +++++++++++ kubernetes/app/archmirror/configmap-sync.yaml | 42 ++++++++++++ kubernetes/app/archmirror/cronjob-sync.yaml | 54 ++++++++++++++++ kubernetes/app/archmirror/deployment.yaml | 64 +++++++++++++++++++ kubernetes/app/archmirror/ingress.yaml | 20 ++++++ kubernetes/app/archmirror/namespace.yaml | 5 ++ kubernetes/app/archmirror/networkpolicy.yaml | 29 +++++++++ kubernetes/app/archmirror/pv.yaml | 18 ++++++ kubernetes/app/archmirror/pvc.yaml | 14 ++++ kubernetes/app/archmirror/service.yaml | 14 ++++ kubernetes/config/cluster-vars.sops.yaml | 9 ++- 11 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 kubernetes/app/archmirror/configmap-nginx.yaml create mode 100644 kubernetes/app/archmirror/configmap-sync.yaml create mode 100644 kubernetes/app/archmirror/cronjob-sync.yaml create mode 100644 kubernetes/app/archmirror/deployment.yaml create mode 100644 kubernetes/app/archmirror/ingress.yaml create mode 100644 kubernetes/app/archmirror/namespace.yaml create mode 100644 kubernetes/app/archmirror/networkpolicy.yaml create mode 100644 kubernetes/app/archmirror/pv.yaml create mode 100644 kubernetes/app/archmirror/pvc.yaml create mode 100644 kubernetes/app/archmirror/service.yaml diff --git a/kubernetes/app/archmirror/configmap-nginx.yaml b/kubernetes/app/archmirror/configmap-nginx.yaml new file mode 100644 index 0000000..e3d2358 --- /dev/null +++ b/kubernetes/app/archmirror/configmap-nginx.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: archmirror-nginx-config + namespace: archmirror +data: + nginx.conf: | + events { + worker_connections 1024; + } + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + gzip on; + + server { + listen 80; + root /usr/share/nginx/html; + + location /archlinux/ { + alias /usr/share/nginx/html/archlinux/; + autoindex on; + } + + location = / { + return 301 /archlinux/; + } + + location /health { + return 200 "OK\n"; + add_header Content-Type text/plain; + } + } + } diff --git a/kubernetes/app/archmirror/configmap-sync.yaml b/kubernetes/app/archmirror/configmap-sync.yaml new file mode 100644 index 0000000..505fada --- /dev/null +++ b/kubernetes/app/archmirror/configmap-sync.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: archmirror-sync-script + namespace: archmirror +data: + sync.sh: | + #!/bin/sh + set -e + + TARGET_DIR="/archlinux" + MAX_RETRIES=3 + + log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2 + } + + if [ -z "$MIRROR_URL" ]; then + log "ERROR: MIRROR_URL not set" + exit 1 + fi + + mkdir -p "$TARGET_DIR" + for i in $(seq 1 $MAX_RETRIES); do + log "Sync attempt $i/$MAX_RETRIES from $MIRROR_URL" + + if rsync --timeout=7200 \ + -rlptH --safe-links --delete-delay --delay-updates \ + -v --info=progress2 \ + "$MIRROR_URL/" "$TARGET_DIR/"; then + log "Sync completed successfully" + exit 0 + fi + + if [ "$i" -lt "$MAX_RETRIES" ]; then + sleep $((i * 300)) + fi + done + + log "All sync attempts failed" + exit 1 diff --git a/kubernetes/app/archmirror/cronjob-sync.yaml b/kubernetes/app/archmirror/cronjob-sync.yaml new file mode 100644 index 0000000..3dbaa91 --- /dev/null +++ b/kubernetes/app/archmirror/cronjob-sync.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: archmirror-sync + namespace: archmirror + labels: + app: archmirror-sync +spec: + schedule: "0 */6 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 14400 + template: + metadata: + labels: + app: archmirror-sync + spec: + restartPolicy: OnFailure + containers: + - name: rsync + image: alpine:3.21 + command: + - sh + - -c + - apk add --no-cache rsync && sh /scripts/sync.sh + env: + - name: MIRROR_URL + value: ${ARCHMIRROR_MIRROR_URL} + volumeMounts: + - name: data + mountPath: /archlinux + - name: sync-script + mountPath: /scripts + readOnly: true + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi + volumes: + - name: data + persistentVolumeClaim: + claimName: archmirror-data + - name: sync-script + configMap: + name: archmirror-sync-script + defaultMode: 0755 diff --git a/kubernetes/app/archmirror/deployment.yaml b/kubernetes/app/archmirror/deployment.yaml new file mode 100644 index 0000000..8622448 --- /dev/null +++ b/kubernetes/app/archmirror/deployment.yaml @@ -0,0 +1,64 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: archmirror + namespace: archmirror + labels: + app: archmirror +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: archmirror + template: + metadata: + labels: + app: archmirror + spec: + containers: + - name: nginx + image: nginx:1.27-alpine + ports: + - containerPort: 80 + name: http + protocol: TCP + livenessProbe: + httpGet: + port: 80 + path: /health + initialDelaySeconds: 5 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + port: 80 + path: /health + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 128Mi + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html/archlinux + readOnly: true + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: archmirror-data + - name: nginx-config + configMap: + name: archmirror-nginx-config diff --git a/kubernetes/app/archmirror/ingress.yaml b/kubernetes/app/archmirror/ingress.yaml new file mode 100644 index 0000000..fba334a --- /dev/null +++ b/kubernetes/app/archmirror/ingress.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: archmirror + namespace: archmirror + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web +spec: + rules: + - host: ${ARCHMIRROR_HOST} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: archmirror + port: + number: 80 diff --git a/kubernetes/app/archmirror/namespace.yaml b/kubernetes/app/archmirror/namespace.yaml new file mode 100644 index 0000000..ce9bd96 --- /dev/null +++ b/kubernetes/app/archmirror/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: archmirror diff --git a/kubernetes/app/archmirror/networkpolicy.yaml b/kubernetes/app/archmirror/networkpolicy.yaml new file mode 100644 index 0000000..0c0626c --- /dev/null +++ b/kubernetes/app/archmirror/networkpolicy.yaml @@ -0,0 +1,29 @@ +--- +# Default deny all ingress in the archmirror namespace +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-ingress + namespace: archmirror +spec: + podSelector: {} + policyTypes: + - Ingress +--- +# Allow Traefik ingress controller to reach nginx +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-controller + namespace: archmirror +spec: + podSelector: + matchLabels: + app: archmirror + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: traefik diff --git a/kubernetes/app/archmirror/pv.yaml b/kubernetes/app/archmirror/pv.yaml new file mode 100644 index 0000000..2d064e4 --- /dev/null +++ b/kubernetes/app/archmirror/pv.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: archmirror-data-nfs +spec: + capacity: + storage: 200Gi + accessModes: + - ReadWriteMany + storageClassName: "" + persistentVolumeReclaimPolicy: Retain + mountOptions: + - hard + - nointr + nfs: + server: synology.storage.lviv + path: ${ARCHMIRROR_NFS_PATH} diff --git a/kubernetes/app/archmirror/pvc.yaml b/kubernetes/app/archmirror/pvc.yaml new file mode 100644 index 0000000..a374e4c --- /dev/null +++ b/kubernetes/app/archmirror/pvc.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: archmirror-data + namespace: archmirror +spec: + accessModes: + - ReadWriteMany + storageClassName: "" + volumeName: archmirror-data-nfs + resources: + requests: + storage: 200Gi diff --git a/kubernetes/app/archmirror/service.yaml b/kubernetes/app/archmirror/service.yaml new file mode 100644 index 0000000..c3fa63a --- /dev/null +++ b/kubernetes/app/archmirror/service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: archmirror + namespace: archmirror +spec: + selector: + app: archmirror + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP diff --git a/kubernetes/config/cluster-vars.sops.yaml b/kubernetes/config/cluster-vars.sops.yaml index 5c652b5..5926e40 100644 --- a/kubernetes/config/cluster-vars.sops.yaml +++ b/kubernetes/config/cluster-vars.sops.yaml @@ -14,6 +14,9 @@ stringData: IMMICH_UPLOAD_NFS_PATH: ENC[AES256_GCM,data:l8F1AkmhGkNxo29X5UER,iv:Z/u0yLNv5ClQu44lPPzGIB2bEsADFCD/mCd+Kw8kuhc=,tag:a8QGaUEYF3iJbZKcAiRKUg==,type:str] JELLYFIN_HOST: ENC[AES256_GCM,data:88I8uzcJa/VwsWOJDe69bUsdGbXzTIGI,iv:TWIALVMMDV9VV7iz0OMsVJ8Cvh13VI54KmACR2utlJI=,tag:yDx1vGk/WfFXaQrnLbhLVA==,type:str] JELLYFIN_INTERNAL_HOST: ENC[AES256_GCM,data:1mG+5+lhwypYm4wcZ3D28SbxzPZs,iv:w7zpUKYnFXJYioyTSGdg4D8Gpc4ei6j6lrDji/+Obsw=,tag:YW/n0n0s1iuDgKu17L+IoA==,type:str] + ARCHMIRROR_HOST: ENC[AES256_GCM,data:gOuABquXQBbb1Fcc3cJ05HJ9nbPtRYJ5q/t+6IhhVQ==,iv:r4N4oWQbpSPYIuclWF5mjnDeNDaCigM5a6eKYPehwCc=,tag:hfXg9Nkf2AHn8GiurrIVhw==,type:str] + ARCHMIRROR_NFS_PATH: ENC[AES256_GCM,data:RHNbu/Jobo8Q5DzKjF4RojvrYQ==,iv:khpEqK0KzdZeZm8qKZ3MJQDk2P799FBCNPOJGx4Tdhk=,tag:CKHeuRZttLRwN6noSaehDQ==,type:str] + ARCHMIRROR_MIRROR_URL: ENC[AES256_GCM,data:cIORJWshvr4fL/OqyvplXllcrMdh3UMrt11cBqwgS12O3wGBgyULJNDcP7c2,iv:8Efs43us8xlUvkafWf15K5wqBoJnYLmC50j094taoFs=,tag:6hV2emMunQ1jOteRCANRsA==,type:str] sops: age: - recipient: age1zffnskvuezntkk703a0pyxsd5m8vx2hm33dr47wdfy8mn4fdw4sqgw0jgc @@ -25,7 +28,7 @@ sops: LzhUN3Z4cExIL1IyS3ZCNWh5aWpLbDgKQ7c3MmLykA00NaLoctKVDfJvPqTqh3Ia cDZJUc6jYJXOJYM6YYyZOYcCL2z8V2RpIfA9sPg8PB2eiipZxjk+Cg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-02-22T20:46:14Z" - mac: ENC[AES256_GCM,data:Avht8eFKRZtfDCRZdyAOLF5yuNXMgWrhRgXpmLCjOmtzQz3O3jcCdiXi/UkLx5MAdjouiFJhs/c+QXixU6DnNpTnSDMWtU5fcMrRUn0PJDiddEq8fkZMcW/dFNM79xsOfeS/PAguEpZ6rE+sgn0VzUC/DS60aYRKvJDiZ8ppBuY=,iv:fRCXsP1Mm6Nmn7OFOBq4ozQ7hyg5nTJ9Fyv9CEfqNCk=,tag:sJm21JvbdGlVuyw51j/Qvg==,type:str] + lastmodified: "2026-02-25T21:55:59Z" + mac: ENC[AES256_GCM,data:2H9ege3TITjTtpIVMbYfx85qdQs4VrjrFHZ+mbAtffVgVWtt20j543QegRoiEnMGXpYeT3mWFMzBCHmcTbRp6B7fxwZ3cBTAe7cCHIrqlQRwcTe40qahwIYZmNqmIb9ZHrhJC/Rx2TVnYlMoks2olgs5NGMyUaVMyrwIRCvTtME=,iv:Gq/+jsPB8CixryfVFgL5wjx66wz7NsshuMIuCj64qOA=,tag:rGH4x+q6tgVv6RuKyJaiXw==,type:str] encrypted_regex: ^(data|stringData|email)$ - version: 3.11.0 + version: 3.12.1