From 7a560f6f97c3f9d12a08d96453df66e128eccb28 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Wed, 2 Sep 2026 19:53:17 -0300 Subject: [PATCH] adding new git-ops structure --- apps/alloy/alloy.yaml | 123 +++++++++ apps/auth/README.md | 99 +++++++ apps/auth/authentik.yaml | 244 ++++++++++++++++++ apps/auth/kyverno-helm.yaml | 24 ++ apps/auth/sso-auth-policy.yaml | 39 +++ apps/cloud/affine.yaml | 128 +++++++++ apps/cloud/cloudreve.yaml | 170 ++++++++++++ apps/cloud/slink.yaml | 135 ++++++++++ apps/cronjobs/renovatebot.yaml | 39 +++ apps/default/archivebox.yaml | 160 ++++++++++++ apps/default/changedetection.yaml | 153 +++++++++++ apps/default/homepage.yaml | 205 +++++++++++++++ apps/default/it-tools.yaml | 66 +++++ apps/default/notepad.yaml | 90 +++++++ apps/default/openwebui.yaml | 107 ++++++++ apps/default/paperless.yaml | 150 +++++++++++ apps/default/playwright.yaml | 130 ++++++++++ apps/default/searxng.yaml | 93 +++++++ apps/default/stirlingpdf.yaml | 112 ++++++++ apps/default/uptimekuma.yaml | 106 ++++++++ apps/default/vaultwarden.yaml | 146 +++++++++++ apps/dev/gitea-runner.yaml | 202 +++++++++++++++ apps/dns/adguard-sync.yaml | 117 +++++++++ apps/dns/adguard.yaml | 171 ++++++++++++ apps/infra/beszel-agent.yaml | 38 +++ apps/infra/beszel.yaml | 96 +++++++ apps/infra/bitwarden-cli.yaml | 144 +++++++++++ apps/infra/code-config.yaml | 112 ++++++++ apps/infra/csi-driver-nfs.yaml | 26 ++ apps/infra/external-secrets-helm.yaml | 11 + apps/infra/file-nginx.yaml | 128 +++++++++ apps/infra/haven-notify.yaml | 85 ++++++ apps/infra/ingress-traefik.yaml | 32 +++ apps/infra/wg-easy.yaml | 126 +++++++++ apps/metallb-system/metallb.yaml | 10 + apps/monitoring/grafana.yaml | 125 +++++++++ apps/monitoring/kube-state-metrics.yaml | 145 +++++++++++ apps/monitoring/loki.yaml | 108 ++++++++ apps/monitoring/nodeexporter.yaml | 56 ++++ apps/monitoring/prometheus.yaml | 130 ++++++++++ apps/root/applicationset.yaml | 46 ++++ apps/root/kustomization.yaml | 5 + bootstrap/address-pool.yaml | 22 ++ bootstrap/argocd-install/argocd-api-user.yaml | 23 ++ bootstrap/argocd-install/ingress.yaml | 17 ++ bootstrap/argocd-install/kustomization.yaml | 12 + .../argocd-install/server-insecure-patch.yaml | 6 + bootstrap/namespaces.yaml | 141 ++++++++++ bootstrap/root-app.yaml | 20 ++ bootstrap/secretstores.yaml | 20 ++ 50 files changed, 4693 insertions(+) create mode 100644 apps/alloy/alloy.yaml create mode 100644 apps/auth/README.md create mode 100644 apps/auth/authentik.yaml create mode 100644 apps/auth/kyverno-helm.yaml create mode 100644 apps/auth/sso-auth-policy.yaml create mode 100644 apps/cloud/affine.yaml create mode 100644 apps/cloud/cloudreve.yaml create mode 100644 apps/cloud/slink.yaml create mode 100644 apps/cronjobs/renovatebot.yaml create mode 100644 apps/default/archivebox.yaml create mode 100644 apps/default/changedetection.yaml create mode 100644 apps/default/homepage.yaml create mode 100644 apps/default/it-tools.yaml create mode 100644 apps/default/notepad.yaml create mode 100644 apps/default/openwebui.yaml create mode 100644 apps/default/paperless.yaml create mode 100644 apps/default/playwright.yaml create mode 100644 apps/default/searxng.yaml create mode 100644 apps/default/stirlingpdf.yaml create mode 100644 apps/default/uptimekuma.yaml create mode 100644 apps/default/vaultwarden.yaml create mode 100644 apps/dev/gitea-runner.yaml create mode 100644 apps/dns/adguard-sync.yaml create mode 100644 apps/dns/adguard.yaml create mode 100644 apps/infra/beszel-agent.yaml create mode 100644 apps/infra/beszel.yaml create mode 100644 apps/infra/bitwarden-cli.yaml create mode 100644 apps/infra/code-config.yaml create mode 100644 apps/infra/csi-driver-nfs.yaml create mode 100644 apps/infra/external-secrets-helm.yaml create mode 100644 apps/infra/file-nginx.yaml create mode 100644 apps/infra/haven-notify.yaml create mode 100644 apps/infra/ingress-traefik.yaml create mode 100644 apps/infra/wg-easy.yaml create mode 100644 apps/metallb-system/metallb.yaml create mode 100644 apps/monitoring/grafana.yaml create mode 100644 apps/monitoring/kube-state-metrics.yaml create mode 100644 apps/monitoring/loki.yaml create mode 100644 apps/monitoring/nodeexporter.yaml create mode 100644 apps/monitoring/prometheus.yaml create mode 100644 apps/root/applicationset.yaml create mode 100644 apps/root/kustomization.yaml create mode 100644 bootstrap/address-pool.yaml create mode 100644 bootstrap/argocd-install/argocd-api-user.yaml create mode 100644 bootstrap/argocd-install/ingress.yaml create mode 100644 bootstrap/argocd-install/kustomization.yaml create mode 100644 bootstrap/argocd-install/server-insecure-patch.yaml create mode 100644 bootstrap/namespaces.yaml create mode 100644 bootstrap/root-app.yaml create mode 100644 bootstrap/secretstores.yaml diff --git a/apps/alloy/alloy.yaml b/apps/alloy/alloy.yaml new file mode 100644 index 0000000..1bc0a9c --- /dev/null +++ b/apps/alloy/alloy.yaml @@ -0,0 +1,123 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +chart: true +metadata: + name: alloy + namespace: argocd + finalizers: [resources-finalizer.argocd.argoproj.io] +spec: + project: default + destination: + server: https://kubernetes.default.svc + namespace: alloy + syncPolicy: + automated: { prune: true, selfHeal: true } + syncOptions: [CreateNamespace=true, ServerSideApply=true] + source: + repoURL: https://grafana.github.io/helm-charts + chart: alloy + targetRevision: 1.12.1 + helm: + valuesObject: + controller: + type: daemonset + alloy: + clustering: { enabled: false } + mounts: + varlog: true + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + configMap: + create: true + content: |- + discovery.kubernetes "all_pods" { + role = "pod" + + selectors { + role = "pod" + field = "spec.nodeName=" + coalesce(env("HOSTNAME"), constants.hostname) + } + } + + discovery.relabel "all_pods" { + targets = discovery.kubernetes.all_pods.targets + + rule { + source_labels = ["__meta_kubernetes_namespace"] + target_label = "namespace" + } + rule { + source_labels = ["__meta_kubernetes_pod_name"] + target_label = "pod" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + target_label = "container" + } + rule { + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"] + target_label = "app" + } + } + + loki.source.kubernetes "all_logs" { + targets = discovery.relabel.all_pods.output + forward_to = [loki.write.main.receiver] + } + + discovery.kubernetes "shared_pods" { + role = "pod" + selectors { + role = "pod" + field = "metadata.namespace=chacal" + } + } + + discovery.relabel "shared_pods" { + targets = discovery.kubernetes.shared_pods.targets + + rule { + source_labels = ["__meta_kubernetes_namespace"] + target_label = "namespace" + } + rule { + source_labels = ["__meta_kubernetes_pod_name"] + target_label = "pod" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + target_label = "container" + } + rule { + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"] + target_label = "app" + } + } + + loki.source.kubernetes "shared_logs" { + targets = discovery.relabel.shared_pods.output + forward_to = [loki.write.shared.receiver] + } + + loki.write "main" { + endpoint { + url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push" + headers = { + "X-Scope-OrgID" = "main", + } + } + } + + loki.write "shared" { + endpoint { + url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push" + headers = { + "X-Scope-OrgID" = "chacal", + } + } + } diff --git a/apps/auth/README.md b/apps/auth/README.md new file mode 100644 index 0000000..743f515 --- /dev/null +++ b/apps/auth/README.md @@ -0,0 +1,99 @@ +# Auth (Authentik + Kyverno SSO annotation policy) + +Draft manifests only — nothing applied yet. Convention: any Ingress annotated +with `use-sso-auth: "true"` gets the Authentik forward-auth annotations injected +by the Kyverno ClusterPolicy (`auth/sso-auth-policy.yaml`). + +## Deploy order + +1. **Authentik** (`auth/authentik.yaml`) — IdP + embedded outpost. Needs a + Postgres user/DB on postgresql.haven first (see below), plus secrets from + Vaultwarden/ESO. +2. **Proxy provider** — create in Authentik UI (or via API) once it's up: + one provider per app you want header-auth'd, mode "forward_auth (single + app)", or a domain-level provider with the embedded outpost. +3. **Kyverno** (`auth/kyverno-helm.yaml`) — policy engine. +4. **SSO policy** (`auth/sso-auth-policy.yaml`) — expands `use-sso-auth: "true"` + into `auth-url` / `auth-signin` / `auth-response-headers` annotations. +5. Annotate ingresses: `kubectl annotate ingress -n use-sso-auth=true` + and mirror the change in this repo. + +## Postgres bootstrap (one-time, on postgresql.haven) + +```sql +CREATE USER authentik WITH PASSWORD ''; +CREATE DATABASE authentik OWNER authentik; +``` + +## Notes + +- Outpost URL used everywhere is `https://auth.haven` — add DNS + ingress for + the outpost before annotating anything. +- Apps that should *consume* the injected `X-Authentik-*` headers (Grafana auth + proxy, Paperless remote-user, OpenWebUI trusted headers) need their own env + changes; those are app-side, not covered by the policy. +- API/WebSocket-heavy apps (arr stack, qBittorrent) should NOT get the + annotation on API paths — either skip the annotation or exclude paths. + +## Which apps get the annotation (scoping) + +Three tiers, applied per ingress — never blanket: + +1. **OIDC tier** (no annotation, wire native OIDC in the app instead): + grafana, paperless, affine, openwebui, vaultwarden, beszel, slink. + Proper logout, group mapping, zero API breakage. +2. **Forward-auth tier** (`use-sso-auth: "true"`): browser-only utilities with + no real auth — it-tools, notepad, searxng, homepage, archivebox, + stirlingpdf, file-nginx, code-config, havenllo, own apps (chacal, + mindforge) if wanted. +3. **Hands off** (built-in auth is fine, forward-auth breaks clients): + sonarr, radarr, prowlarr, qbittorrent, adguard, changedetection, + uptimekuma, cloudreve (WebDAV), jellyfin (clients can't do redirects — + only the community SSO plugin route exists). + +## Pre-annotation checklist (per new app) + +Forward-auth breakage is loud and immediate if you look in the right places. +Before annotating any real hostname: + +1. **Ask: what talks to this app that isn't a human in a browser?** + Mobile/desktop apps with own login, API-key consumers, push/webhook + receivers, cronjobs curling through the ingress, WebDAV/RSS → any of + those = hands off (tier 3) or exclude paths. "Just me in a browser" = + safe. +2. **Canary first:** create a scratch ingress for the same service + (`-test.haven`) with the annotation; leave the original untouched + for ~a week. Forgotten integrations keep hitting the original and only + break the canary — instant rollback + (`kubectl annotate ingress -n use-sso-auth-`). +3. **Grep the docs** (30s): "reverse proxy", "trusted header", "API key", + "webhook", "basic auth". A "running behind a reverse proxy" doc section + is where landmines are documented. +4. **Header-trust hygiene:** apps consuming `X-Authentik-*` must be + reachable ONLY through the ingress (no NodePort/exposed port, otherwise + LAN clients can forge headers straight to the pod). Keep built-in local + login enabled; never set SSO-only mode. + +## Files + +- `kyverno-helm.yaml` — Kyverno install values +- `sso-auth-policy.yaml` — ClusterPolicy: `use-sso-auth` → nginx auth annotations +- `authentik.yaml` — Authentik server/worker + outpost, Postgres PVC, ingress + +## Status / when to actually deploy this (decision from 2026-08-28) + +NOT deployed — deliberate. Sole user, low login frequency, everything +reachable via wg-easy, so SSO solves a problem that doesn't exist. The +`*.ivanch.me` public ingresses were reviewed instead: exposure without usage +is pure risk, and the right fix is removing exposure (VPN-only), not adding +an IdP. + +Pull the trigger only if one of these becomes true: + +1. A second person uses the homelab regularly (SSO value scales with users). +2. Something must be exposed that can't sit behind VPN (third-party + webhooks/callbacks) — then public tier gets `use-sso-auth` + 2FA. +3. Login friction on the OIDC apps (grafana/paperless/affine/openwebui/ + vaultwarden/beszel/slink) actually annoys on a weekly basis. + +Until then this folder is a shelf-ready draft; cost of keeping it is zero. diff --git a/apps/auth/authentik.yaml b/apps/auth/authentik.yaml new file mode 100644 index 0000000..afdf840 --- /dev/null +++ b/apps/auth/authentik.yaml @@ -0,0 +1,244 @@ +# Authentik — IdP + embedded outpost (proxy provider forward-auth). +# Draft — do NOT apply as-is: +# - secret values are placeholders; create them from Vaultwarden/ESO first +# - requires the authentik user/DB on postgresql.haven (see README.md) +# - requires DNS record for auth.haven -> ingress IP (user manages DNS) +--- +apiVersion: v1 +kind: Secret +metadata: + name: authentik-secrets + namespace: auth +type: Opaque +stringData: + postgres_password: "CHANGE_ME" # Vaultwarden -> ESO later + secret_key: "CHANGE_ME" # `openssl rand -base64 60` + bootstrap_password: "CHANGE_ME" # initial akadmin password + bootstrap_token: "CHANGE_ME" # outpost token +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: authentik-media + namespace: auth +spec: + accessModes: [ReadWriteOnce] + storageClassName: nfs-fast-client # verified: exists in cluster (nfs-client is also default) + resources: + requests: + storage: 2Gi +--- +# Server (web + API) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-server + namespace: auth +spec: + replicas: 1 + selector: + matchLabels: { app: authentik, component: server } + template: + metadata: + labels: { app: authentik, component: server } + spec: + containers: + - name: server + image: ghcr.io/goauthentik/server:2026.8 + args: [server] + env: + - name: AUTHENTIK_SECRET_KEY + valueFrom: + { secretKeyRef: { name: authentik-secrets, key: secret_key } } + - name: AUTHENTIK_POSTGRESQL__HOST + value: postgresql.haven + - name: AUTHENTIK_POSTGRESQL__NAME + value: authentik + - name: AUTHENTIK_POSTGRESQL__USER + value: authentik + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + { + secretKeyRef: + { name: authentik-secrets, key: postgres_password }, + } + - name: AUTHENTIK_BOOTSTRAP_PASSWORD + valueFrom: + { + secretKeyRef: + { name: authentik-secrets, key: bootstrap_password }, + } + - name: AUTHENTIK_BOOTSTRAP_TOKEN + valueFrom: + { + secretKeyRef: + { name: authentik-secrets, key: bootstrap_token }, + } + - name: AUTHENTIK_ERROR_REPORTING__ENABLED + value: "false" + ports: + - { containerPort: 9000, name: http } + - { containerPort: 9443, name: https } + readinessProbe: + httpGet: { path: /-/health/ready/, port: 9000 } + initialDelaySeconds: 20 + periodSeconds: 10 + livenessProbe: + httpGet: { path: /-/health/live/, port: 9000 } + initialDelaySeconds: 40 + periodSeconds: 20 + resources: + requests: { cpu: 250m, memory: 512Mi } + limits: { memory: 1Gi, cpu: 1000m } + volumeMounts: + - { name: media, mountPath: /media } + volumes: + - name: media + persistentVolumeClaim: { claimName: authentik-media } +--- +# Worker (policies, outpost management, scheduled tasks) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-worker + namespace: auth +spec: + replicas: 1 + selector: + matchLabels: { app: authentik, component: worker } + template: + metadata: + labels: { app: authentik, component: worker } + spec: + containers: + - name: worker + image: ghcr.io/goauthentik/server:2026.8 + args: [worker] + env: # same env as server; kept duplicated for a flat draft manifest + - name: AUTHENTIK_SECRET_KEY + valueFrom: + { secretKeyRef: { name: authentik-secrets, key: secret_key } } + - name: AUTHENTIK_POSTGRESQL__HOST + value: postgresql.haven + - name: AUTHENTIK_POSTGRESQL__NAME + value: authentik + - name: AUTHENTIK_POSTGRESQL__USER + value: authentik + - name: AUTHENTIK_POSTGRESQL__PASSWORD + valueFrom: + { + secretKeyRef: + { name: authentik-secrets, key: postgres_password }, + } + - name: AUTHENTIK_BOOTSTRAP_PASSWORD + valueFrom: + { + secretKeyRef: + { name: authentik-secrets, key: bootstrap_password }, + } + - name: AUTHENTIK_BOOTSTRAP_TOKEN + valueFrom: + { + secretKeyRef: + { name: authentik-secrets, key: bootstrap_token }, + } + - name: AUTHENTIK_ERROR_REPORTING__ENABLED + value: "false" + resources: + requests: { cpu: 200m, memory: 512Mi } + limits: { memory: 1Gi, cpu: 1000m } +--- +apiVersion: v1 +kind: Service +metadata: + name: authentik + namespace: auth +spec: + selector: { app: authentik, component: server } + ports: + - { name: http, port: 80, targetPort: 9000 } +--- +# Embedded outpost — runs the proxy providers; this is what ingress-nginx +# calls for /auth/nginx on every use-sso-auth ingress. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: authentik-outpost + namespace: auth +spec: + replicas: 1 + selector: + matchLabels: { app: authentik, component: outpost } + template: + metadata: + labels: { app: authentik, component: outpost } + spec: + containers: + - name: outpost + image: ghcr.io/goauthentik/proxy:2026.8 + env: + - name: AUTHENTIK_HOST + value: http://authentik.auth.svc.cluster.local + - name: AUTHENTIK_INSECURE + value: "true" + - name: AUTHENTIK_TOKEN + valueFrom: + { + secretKeyRef: + { name: authentik-secrets, key: bootstrap_token }, + } + ports: + - { containerPort: 9000, name: http } + readinessProbe: + httpGet: { path: /outpost.goauthentik.io/ping, port: 9000 } + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + requests: { cpu: 200m, memory: 512Mi } + limits: { memory: 1Gi, cpu: 1000m } +--- +apiVersion: v1 +kind: Service +metadata: + name: authentik-outpost + namespace: auth +spec: + selector: { app: authentik, component: outpost } + ports: + - { name: http, port: 80, targetPort: 9000 } +--- +# SSO portal (user-facing login) — auth.haven +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: authentik + namespace: auth +spec: + ingressClassName: traefik + rules: + - host: auth.haven + http: + paths: + - path: / + pathType: Prefix + backend: { service: { name: authentik, port: { number: 80 } } } +--- +# Outpost route — the /outpost.goauthentik.io path must resolve on the same +# host the protected apps redirect to. Kept as a separate ingress so it can +# also be attached to other hosts later if needed (do NOT give it +# use-sso-auth — that would create an auth loop). +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: authentik-outpost + namespace: auth +spec: + ingressClassName: traefik + rules: + - host: auth.haven + http: + paths: + - path: /outpost.goauthentik.io + pathType: Prefix + backend: + { service: { name: authentik-outpost, port: { number: 80 } } } diff --git a/apps/auth/kyverno-helm.yaml b/apps/auth/kyverno-helm.yaml new file mode 100644 index 0000000..00ff76b --- /dev/null +++ b/apps/auth/kyverno-helm.yaml @@ -0,0 +1,24 @@ +# Kyverno — policy engine. Install with: +# helm repo add kyverno https://kyverno.github.io/kyverno +# helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace -f auth/kyverno-helm.yaml +# The only consumer of Kyverno here is the use-sso-auth mutation policy; +# if you later drop it, Kyverno can be removed with no other impact. +admissionController: + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 256Mi +backgroundController: + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 128Mi +cleanupController: + enabled: false +reportsController: + enabled: false diff --git a/apps/auth/sso-auth-policy.yaml b/apps/auth/sso-auth-policy.yaml new file mode 100644 index 0000000..4326bef --- /dev/null +++ b/apps/auth/sso-auth-policy.yaml @@ -0,0 +1,39 @@ +# ClusterPolicy: expands the single `use-sso-auth: "true"` Ingress annotation +# into the ingress-nginx forward-auth annotations pointed at the Authentik +# embedded outpost. +# +# Test before applying to real ingresses: +# kubectl apply -f auth/sso-auth-policy.yaml +# kubectl annotate ingress -n default homepage use-sso-auth=true --dry-run=server -o yaml # check annotations get injected +# # remove the test annotation afterwards +--- +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: inject-sso-auth +spec: + validationFailureAction: Audit + background: false + rules: + - name: authentik-forward-auth + match: + any: + - resources: + kinds: [Ingress] + preconditions: + all: + - key: "{{ request.object.metadata.annotations.\"use-sso-auth\" || '' }}" + operator: Equals + value: "true" + mutate: + patchStrategicMerge: + metadata: + annotations: + nginx.ingress.kubernetes.io/auth-url: https://auth.haven/outpost.goauthentik.io/auth/nginx + nginx.ingress.kubernetes.io/auth-signin: https://auth.haven/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri + # NOTE: no auth-snippet used — ingress-nginx 1.15 rejects snippet + # annotations by default (allow-snippet-annotations=false, your + # controller ConfigMap is empty so it uses the default). + # ingress-nginx already forwards X-Original-URL to the auth + # backend automatically, which is all Authentik needs. + nginx.ingress.kubernetes.io/auth-response-headers: X-Authentik-Username,X-Authentik-Email,X-Authentik-Groups,X-Authentik-Name,X-Authentik-Metadata diff --git a/apps/cloud/affine.yaml b/apps/cloud/affine.yaml new file mode 100644 index 0000000..1f87239 --- /dev/null +++ b/apps/cloud/affine.yaml @@ -0,0 +1,128 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: affine + namespace: cloud + labels: + app: affine +spec: + replicas: 1 + selector: + matchLabels: + app: affine + strategy: + type: Recreate + template: + metadata: + labels: + app: affine + spec: + containers: + - name: affine + image: ghcr.io/toeverything/affine:stable + imagePullPolicy: Always + command: + [ + "sh", + "-c", + "node ./scripts/self-host-predeploy.js && node ./dist/main.js", + ] + env: + - name: AFFINE_CONFIG_PATH + value: "/root/.affine/config" + - name: AFFINE_SERVER_EXTERNAL_URL + value: "http://affine.haven" + - name: AFFINE_SERVER_HOST + value: "0.0.0.0" + - name: AFFINE_SERVER_PORT + value: "3010" + - name: AFFINE_SERVER_HTTPS + value: "false" + - name: AFFINE_SERVER_SUBPATH + value: "/" + - name: AFFINE_ENABLE_SYNC_FROM_STARTUP + value: "true" + - name: DATABASE_URL + value: "postgres://affine:affine@postgresql.haven:5432/affine" + - name: REDIS_SERVER_HOST + value: "redis.haven" + - name: REDIS_SERVER_PORT + value: "6379" + ports: + - containerPort: 3010 + name: http + readinessProbe: + httpGet: + path: / + port: 3010 + initialDelaySeconds: 20 + periodSeconds: 10 + failureThreshold: 6 + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 2000m + memory: 2Gi + volumeMounts: + - name: config + mountPath: /root/.affine/config + volumes: + - name: config + persistentVolumeClaim: + claimName: affine-config + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: affine + namespace: cloud + labels: + app: affine +spec: + type: ClusterIP + selector: + app: affine + ports: + - name: http + port: 3010 + protocol: TCP + targetPort: http +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: affine-config + namespace: cloud + annotations: + nfs.io/storage-path: "affine-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: affine + namespace: cloud + labels: + app: affine +spec: + rules: + - host: affine.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: affine + port: + number: 3010 diff --git a/apps/cloud/cloudreve.yaml b/apps/cloud/cloudreve.yaml new file mode 100644 index 0000000..b1e3299 --- /dev/null +++ b/apps/cloud/cloudreve.yaml @@ -0,0 +1,170 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cloudreve + namespace: cloud +spec: + replicas: 1 + selector: + matchLabels: + app: cloudreve + template: + metadata: + labels: + app: cloudreve + spec: + containers: + - name: cloudreve + image: cloudreve/cloudreve:v4 + imagePullPolicy: Always + ports: + - containerPort: 5212 + name: http + - containerPort: 6888 + name: slave-tcp + protocol: TCP + - containerPort: 6888 + name: slave-udp + protocol: UDP + env: + - name: CR_CONF_Database.Type + value: "postgres" + - name: CR_CONF_Database.Host + value: "postgresql.haven" + - name: CR_CONF_Database.Port + value: "5432" + - name: CR_CONF_Database.User + valueFrom: + secretKeyRef: + name: cloudreve-secret + key: DB_USER + - name: CR_CONF_Database.Name + valueFrom: + secretKeyRef: + name: cloudreve-secret + key: DB_NAME + - name: CR_CONF_Database.Password + valueFrom: + secretKeyRef: + name: cloudreve-secret + key: DB_PASSWORD + - name: CR_CONF_Database.SSLMode + value: "disable" + - name: CR_CONF_Redis.Server + value: "" + - name: CR_CONF_Redis.Password + valueFrom: + secretKeyRef: + name: cloudreve-secret + key: REDIS_PASSWORD + volumeMounts: + - name: cloudreve-data + mountPath: /cloudreve/data + resources: + requests: + cpu: "250m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "1Gi" + volumes: + - name: cloudreve-data + nfs: + server: 192.168.15.99 + path: /export/Storage/Cloud +--- +apiVersion: v1 +kind: Service +metadata: + name: cloudreve + namespace: cloud +spec: + type: ClusterIP + selector: + app: cloudreve + ports: + - port: 5212 + targetPort: 5212 + name: http + +--- +apiVersion: v1 +kind: Service +metadata: + name: cloudreve-slave + namespace: cloud +spec: + type: ClusterIP + selector: + app: cloudreve + ports: + - port: 6888 + targetPort: 6888 + name: slave-tcp + protocol: TCP + - port: 6888 + targetPort: 6888 + name: slave-udp + protocol: UDP + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: cloudreve-data + namespace: cloud + annotations: + nfs.io/storage-path: "cloudreve-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + limits: + storage: 50Gi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cloudreve + namespace: cloud + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" +spec: + rules: + - host: cloud.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: cloudreve + port: + number: 5212 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: cloudreve-public + namespace: cloud + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" +spec: + rules: + - host: cloud.ivanch.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: cloudreve + port: + number: 5212 diff --git a/apps/cloud/slink.yaml b/apps/cloud/slink.yaml new file mode 100644 index 0000000..7e07e37 --- /dev/null +++ b/apps/cloud/slink.yaml @@ -0,0 +1,135 @@ +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: slink + namespace: cloud +spec: + replicas: 1 + selector: + matchLabels: + app: slink + template: + metadata: + labels: + app: slink + spec: + containers: + - name: slink + image: anirdev/slink:latest + imagePullPolicy: Always + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: ORIGIN + value: "http://slink.haven" + - name: TZ + value: "America/Sao_Paulo" + - name: USER_APPROVAL_REQUIRED + value: "true" + - name: USER_PASSWORD_MIN_LENGTH + value: "8" + - name: USER_PASSWORD_REQUIREMENTS + value: "15" + - name: ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: slink-secret + key: ADMIN_USERNAME + - name: ADMIN_EMAIL + valueFrom: + secretKeyRef: + name: slink-secret + key: ADMIN_EMAIL + - name: ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: slink-secret + key: ADMIN_PASSWORD + - name: IMAGE_MAX_SIZE + value: "50M" + - name: IMAGE_STRIP_EXIF_METADATA + value: "true" + - name: IMAGE_COMPRESSION_QUALITY + value: "90" + - name: STORAGE_PROVIDER + value: "local" + ports: + - containerPort: 3000 + name: slink-port + resources: + requests: + cpu: "100m" + memory: "64Mi" + limits: + cpu: "500m" + memory: "512Mi" + volumeMounts: + - name: slink-data + mountPath: /app/var/data + - name: slink-data + mountPath: /app/slink/images + volumes: + - name: slink-data + persistentVolumeClaim: + claimName: slink-data +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: slink + namespace: cloud +spec: + type: ClusterIP + selector: + app: slink + ports: + - port: 3000 + targetPort: slink-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: slink-data + namespace: cloud + annotations: + nfs.io/storage-path: "slink-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + limits: + storage: 15Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: slink + namespace: cloud + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" +spec: + rules: + - host: slink.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: slink + port: + number: 3000 diff --git a/apps/cronjobs/renovatebot.yaml b/apps/cronjobs/renovatebot.yaml new file mode 100644 index 0000000..bd694ce --- /dev/null +++ b/apps/cronjobs/renovatebot.yaml @@ -0,0 +1,39 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: renovate + namespace: cronjobs +spec: + schedule: "0 12 * * 0" # every Sunday 12:00 + concurrencyPolicy: Forbid + jobTemplate: + spec: + template: + spec: + containers: + - name: renovate + image: renovate/renovate:44.39.3 + args: + - ivanch/haven + env: + - name: LOG_LEVEL + value: debug + - name: RENOVATE_AUTODISCOVER + value: "false" + - name: RENOVATE_PLATFORM + value: "gitea" + - name: RENOVATE_ENDPOINT + value: "https://git.ivanch.me" + - name: RENOVATE_GIT_AUTHOR + value: "Renovate Bot " + - name: RENOVATE_TOKEN + valueFrom: + secretKeyRef: + name: renovate-bot + key: RENOVATE_TOKEN + - name: RENOVATE_GITHUB_COM_TOKEN + valueFrom: + secretKeyRef: + name: renovate-bot + key: RENOVATE_GITHUB_COM_TOKEN + restartPolicy: Never diff --git a/apps/default/archivebox.yaml b/apps/default/archivebox.yaml new file mode 100644 index 0000000..da33cb5 --- /dev/null +++ b/apps/default/archivebox.yaml @@ -0,0 +1,160 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sonic + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: sonic + template: + metadata: + labels: + app: sonic + spec: + containers: + - name: sonic + image: archivebox/sonic:latest + imagePullPolicy: Always + ports: + - containerPort: 1491 + env: + - name: SEARCH_BACKEND_PASSWORD + valueFrom: + secretKeyRef: + name: password + key: password + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "200m" +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: archivebox + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: archivebox + template: + metadata: + labels: + app: archivebox + spec: + containers: + - name: archivebox + image: archivebox/archivebox:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + env: + - name: SONIC_HOST + value: "sonic.default.svc.cluster.local" + - name: SONIC_PORT + value: "1491" + - name: SEARCH_BACKEND_ENGINE + value: "sonic" + - name: SONIC_PASSWORD + valueFrom: + secretKeyRef: + name: archivebox-password + key: password + - name: ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: archivebox-password + key: password + - name: ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: archivebox-password + key: password + - name: CSRF_TRUSTED_ORIGINS + value: "archive.haven" + - name: ALLOWED_HOSTS + value: "*" + - name: PUBLIC_ADD_VIEW + value: "false" + volumeMounts: + - name: archivebox-data + mountPath: /data + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "2Gi" + cpu: "3000m" + volumes: + - name: archivebox-data + persistentVolumeClaim: + claimName: archivebox-data +--- +apiVersion: v1 +kind: Service +metadata: + name: sonic-svc + namespace: default +spec: + selector: + app: sonic + ports: + - protocol: TCP + port: 1491 + targetPort: 1491 +--- +apiVersion: v1 +kind: Service +metadata: + name: archivebox-svc + namespace: default +spec: + selector: + app: archivebox + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: archivebox-data + namespace: default + annotations: + nfs.io/storage-path: "archivebox-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + limits: + storage: 30Gi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: archivebox-ingress + namespace: default +spec: + rules: + - host: "archive.haven" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: archivebox-svc + port: + number: 8000 diff --git a/apps/default/changedetection.yaml b/apps/default/changedetection.yaml new file mode 100644 index 0000000..44128fd --- /dev/null +++ b/apps/default/changedetection.yaml @@ -0,0 +1,153 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: changedetection + namespace: default + labels: + app.kubernetes.io/name: changedetection +spec: + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app.kubernetes.io/name: changedetection + strategy: + type: Recreate + template: + metadata: + labels: + app.kubernetes.io/name: changedetection + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - iris + containers: + - name: changedetection + image: lscr.io/linuxserver/changedetection.io:latest + imagePullPolicy: Always + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "Etc/UTC" + - name: BASE_URL + value: "http://change.haven/" + - name: PLAYWRIGHT_DRIVER_URL + value: "ws://localhost:3000" + ports: + - containerPort: 5000 + name: http + protocol: TCP + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 2000m + memory: 1Gi + securityContext: + allowPrivilegeEscalation: false + runAsUser: 0 + volumeMounts: + - name: config + mountPath: /config + - name: browser-sockpuppet-chrome + image: dgtlmoon/sockpuppetbrowser:latest + imagePullPolicy: Always + env: + - name: SCREEN_WIDTH + value: "1920" + - name: SCREEN_HEIGHT + value: "1024" + - name: SCREEN_DEPTH + value: "16" + - name: MAX_CONCURRENT_CHROME_PROCESSES + value: "10" + ports: + - containerPort: 3000 + name: ws + protocol: TCP + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 2000m + memory: 4Gi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - SYS_ADMIN + drop: + - ALL + volumes: + - name: config + persistentVolumeClaim: + claimName: changedetection-config + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: changedetection + namespace: default + labels: + app.kubernetes.io/name: changedetection +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: changedetection + ports: + - name: http + port: 5000 + protocol: TCP + targetPort: http +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: changedetection-config + namespace: default + annotations: + nfs.io/storage-path: "changedetection-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + limits: + storage: 2Gi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: changedetection + namespace: default + labels: + app.kubernetes.io/name: changedetection +spec: + rules: + - host: change.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: changedetection + port: + number: 5000 diff --git a/apps/default/homepage.yaml b/apps/default/homepage.yaml new file mode 100644 index 0000000..04fc585 --- /dev/null +++ b/apps/default/homepage.yaml @@ -0,0 +1,205 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: homepage + namespace: default + labels: + app.kubernetes.io/name: homepage +secrets: + - name: homepage +--- +apiVersion: v1 +kind: Secret +type: kubernetes.io/service-account-token +metadata: + name: homepage + namespace: default + labels: + app.kubernetes.io/name: homepage + annotations: + kubernetes.io/service-account.name: homepage +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: homepage + labels: + app.kubernetes.io/name: homepage +rules: + - apiGroups: + - "" + resources: + - namespaces + - pods + - nodes + verbs: + - get + - list + - apiGroups: + - extensions + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - apiGroups: + - traefik.io + resources: + - ingressroutes + verbs: + - get + - list + - apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes + - gateways + verbs: + - get + - list + - apiGroups: + - metrics.k8s.io + resources: + - nodes + - pods + verbs: + - get + - list +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: homepage + labels: + app.kubernetes.io/name: homepage +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: homepage +subjects: + - kind: ServiceAccount + name: homepage + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: homepage + namespace: default + labels: + app.kubernetes.io/name: homepage +spec: + revisionHistoryLimit: 3 + replicas: 1 + strategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: homepage + template: + metadata: + labels: + app.kubernetes.io/name: homepage + annotations: + configmap.reloader/checksum: '{{ include (print $.Template.BasePath "/app/config/services.yaml") . | sha256sum }}' + spec: + serviceAccountName: homepage + automountServiceAccountToken: true + enableServiceLinks: true + containers: + - name: homepage + image: "ghcr.io/gethomepage/homepage:latest" + imagePullPolicy: Always + env: + - name: HOMEPAGE_ALLOWED_HOSTS + value: "*" + ports: + - name: http + containerPort: 3000 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + volumeMounts: + - name: logs + mountPath: /app/config/logs + - name: homepage-config + mountPath: /app/config + - name: homepage-config + mountPath: /app/public/images + subPath: images + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: homepage-config + persistentVolumeClaim: + claimName: homepage-config + - name: logs + emptyDir: {} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: homepage-config + namespace: default + annotations: + nfs.io/storage-path: "homepage-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: homepage + namespace: default + labels: + app.kubernetes.io/name: homepage +spec: + type: ClusterIP + ports: + - port: 3000 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: homepage +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: homepage + namespace: default + labels: + app.kubernetes.io/name: homepage +spec: + rules: + - host: "homepage.haven" + http: + paths: + - path: "/" + pathType: Prefix + backend: + service: + name: homepage + port: + number: 3000 diff --git a/apps/default/it-tools.yaml b/apps/default/it-tools.yaml new file mode 100644 index 0000000..164956d --- /dev/null +++ b/apps/default/it-tools.yaml @@ -0,0 +1,66 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: it-tools + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: it-tools + template: + metadata: + labels: + app: it-tools + spec: + containers: + - name: it-tools + image: corentinth/it-tools:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "200m" +--- +apiVersion: v1 +kind: Service +metadata: + name: it-tools-svc + namespace: default +spec: + selector: + app: it-tools + ports: + - protocol: TCP + port: 80 + targetPort: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: it-tools-ingress + namespace: default +spec: + rules: + - host: "tools.haven" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: it-tools-svc + port: + number: 80 diff --git a/apps/default/notepad.yaml b/apps/default/notepad.yaml new file mode 100644 index 0000000..030b1d8 --- /dev/null +++ b/apps/default/notepad.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: notepad + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: notepad + template: + metadata: + labels: + app: notepad + spec: + containers: + - name: notepad + image: jdreinhardt/minimalist-web-notepad:latest + imagePullPolicy: Always + command: + - sh + - -c + - >- + mkdir -p /var/www/html/_tmp && + cp -n /var/www/html/notes.htaccess /var/www/html/_tmp/.htaccess 2>/dev/null; + exec docker-php-entrypoint apache2-foreground + ports: + - containerPort: 80 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + volumeMounts: + - name: notepad-data + mountPath: /var/www/html/_tmp + volumes: + - name: notepad-data + persistentVolumeClaim: + claimName: notepad-data + +--- +apiVersion: v1 +kind: Service +metadata: + name: notepad + namespace: default +spec: + type: ClusterIP + selector: + app: notepad + ports: + - port: 80 + targetPort: 80 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: notepad-data + namespace: default + annotations: + nfs.io/storage-path: "notepad-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: notepad + namespace: default +spec: + rules: + - host: notepad.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: notepad + port: + number: 80 diff --git a/apps/default/openwebui.yaml b/apps/default/openwebui.yaml new file mode 100644 index 0000000..18af0dd --- /dev/null +++ b/apps/default/openwebui.yaml @@ -0,0 +1,107 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: openwebui + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: openwebui + template: + metadata: + labels: + app: openwebui + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - iris + containers: + - name: openwebui + image: ghcr.io/open-webui/open-webui:main-slim + imagePullPolicy: Always + ports: + - containerPort: 8080 + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: TZ + value: "America/Sao_Paulo" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: openwebui-secret + key: DATABASE_URL + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "1000m" + memory: "2Gi" + volumeMounts: + - name: openwebui-data + mountPath: /app/backend/data + volumes: + - name: openwebui-data + persistentVolumeClaim: + claimName: openwebui-data +--- +apiVersion: v1 +kind: Service +metadata: + name: openwebui + namespace: default +spec: + type: ClusterIP + selector: + app: openwebui + ports: + - port: 8080 + targetPort: 8080 +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: openwebui-data + namespace: default + annotations: + nfs.io/storage-path: "openwebui-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + limits: + storage: 10Gi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: openwebui + namespace: default + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "50m" +spec: + rules: + - host: openwebui.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: openwebui + port: + number: 8080 diff --git a/apps/default/paperless.yaml b/apps/default/paperless.yaml new file mode 100644 index 0000000..5b4e745 --- /dev/null +++ b/apps/default/paperless.yaml @@ -0,0 +1,150 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paperless + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: paperless + template: + metadata: + labels: + app: paperless + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - iris + containers: + - name: paperless + image: ghcr.io/paperless-ngx/paperless-ngx:latest + imagePullPolicy: Always + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: PAPERLESS_URL + value: "http://paperless.haven" + - name: PAPERLESS_TIME_ZONE + value: "America/Sao_Paulo" + - name: PAPERLESS_OCR_LANGUAGE + value: "por" + - name: PAPERLESS_OCR_LANGUAGES + value: "por" + - name: PAPERLESS_OCR_USER_ARGS + value: '{"invalidate_digital_signatures": true}' + - name: PAPERLESS_DBHOST + value: postgresql.haven + - name: PAPERLESS_DBNAME + valueFrom: + secretKeyRef: + name: paperless-secret + key: PAPERLESS_DBNAME + - name: PAPERLESS_DBUSER + valueFrom: + secretKeyRef: + name: paperless-secret + key: PAPERLESS_DBUSER + - name: PAPERLESS_DBPASSWORD + valueFrom: + secretKeyRef: + name: paperless-secret + key: PAPERLESS_DBPASSWORD + - name: PAPERLESS_REDIS + value: "redis://redis.haven:6379" + - name: PAPERLESS_PORT + value: "8000" + - name: PAPERLESS_SECRET_KEY + valueFrom: + secretKeyRef: + name: paperless-secret + key: PAPERLESS_SECRET_KEY + ports: + - containerPort: 8000 + name: paperless-port + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "4000m" + memory: "1Gi" + volumeMounts: + - name: paperless-data + subPath: data + mountPath: /usr/src/paperless/data + - name: paperless-data + subPath: media + mountPath: /usr/src/paperless/media + - name: paperless-data + subPath: export + mountPath: /usr/src/paperless/export + - name: paperless-data + subPath: consume + mountPath: /usr/src/paperless/consume + volumes: + - name: paperless-data + persistentVolumeClaim: + claimName: paperless-data +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: paperless + namespace: default +spec: + type: ClusterIP + selector: + app: paperless + ports: + - port: 8000 + targetPort: paperless-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: paperless-data + namespace: default + annotations: + nfs.io/storage-path: "paperless-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5Gi + limits: + storage: 15Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: paperless + namespace: default +spec: + rules: + - host: paperless.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: paperless + port: + number: 8000 diff --git a/apps/default/playwright.yaml b/apps/default/playwright.yaml new file mode 100644 index 0000000..94e4840 --- /dev/null +++ b/apps/default/playwright.yaml @@ -0,0 +1,130 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: playwright + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: playwright + strategy: + type: Recreate + template: + metadata: + labels: + app: playwright + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - iris + weight: 100 + containers: + - command: + - npx + - -y + - playwright@1.58.0 + - run-server + - --port + - "3000" + - --host + - 0.0.0.0 + env: + - name: TZ + value: America/Sao_Paulo + image: mcr.microsoft.com/playwright:v1.62.1-noble + imagePullPolicy: Always + name: playwright + ports: + - containerPort: 3000 + protocol: TCP + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: 500m + memory: 512Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /dev/shm + name: dshm + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: + medium: Memory + name: dshm +status: + availableReplicas: 1 + conditions: + - lastTransitionTime: "2026-05-29T15:54:24Z" + lastUpdateTime: "2026-07-22T10:14:35Z" + message: ReplicaSet "playwright-86c74d7c78" has successfully progressed. + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + - lastTransitionTime: "2026-08-28T06:04:14Z" + lastUpdateTime: "2026-08-28T06:04:14Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + observedGeneration: 95 + readyReplicas: 1 + replicas: 1 + terminatingReplicas: 0 + updatedReplicas: 1 +--- +apiVersion: v1 +kind: Service +metadata: + name: playwright + namespace: default +spec: + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - port: 3000 + protocol: TCP + targetPort: 3000 + selector: + app: playwright + sessionAffinity: None + type: ClusterIP +status: + loadBalancer: {} +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: playwright + namespace: default +spec: + rules: + - host: playwright.haven + http: + paths: + - backend: + service: + name: playwright + port: + number: 3000 + path: / + pathType: Prefix +status: + loadBalancer: + ingress: + - ip: 192.168.20.204 diff --git a/apps/default/searxng.yaml b/apps/default/searxng.yaml new file mode 100644 index 0000000..1e9c34e --- /dev/null +++ b/apps/default/searxng.yaml @@ -0,0 +1,93 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: searxng + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: searxng + template: + metadata: + labels: + app: searxng + spec: + enableServiceLinks: false + containers: + - name: searxng + image: searxng/searxng:latest + imagePullPolicy: Always + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + ports: + - containerPort: 8080 + name: searxng-port + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + volumeMounts: + - name: searxng-config + mountPath: /etc/searxng + volumes: + - name: searxng-config + persistentVolumeClaim: + claimName: searxng-config +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: searxng + namespace: default +spec: + type: ClusterIP + selector: + app: searxng + ports: + - port: 8080 + targetPort: searxng-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: searxng-config + namespace: default + annotations: + nfs.io/storage-path: "searxng-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: searxng + namespace: default +spec: + rules: + - host: search.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: searxng + port: + number: 8080 diff --git a/apps/default/stirlingpdf.yaml b/apps/default/stirlingpdf.yaml new file mode 100644 index 0000000..b7da989 --- /dev/null +++ b/apps/default/stirlingpdf.yaml @@ -0,0 +1,112 @@ +--- +# 1) Deployment - Stirling-PDF +apiVersion: apps/v1 +kind: Deployment +metadata: + name: stirlingpdf + namespace: default + labels: + app: stirlingpdf +spec: + replicas: 1 + selector: + matchLabels: + app: stirlingpdf + template: + metadata: + labels: + app: stirlingpdf + spec: + containers: + - name: stirlingpdf + image: stirlingtools/stirling-pdf:latest + imagePullPolicy: Always + env: + - name: TZ + value: "America/Sao_Paulo" + - name: DOCKER_ENABLE_SECURITY + value: "false" + - name: SECURITY_ENABLELOGIN + value: "false" + ports: + - containerPort: 8080 + name: http + readinessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 20 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 1Gi + limits: + cpu: 2000m + memory: 2Gi + securityContext: + allowPrivilegeEscalation: false + runAsUser: 0 + volumeMounts: + - name: config + mountPath: /configs + volumes: + - name: config + persistentVolumeClaim: + claimName: stirlingpdf-config + terminationGracePeriodSeconds: 30 +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: stirlingpdf + namespace: default + labels: + app: stirlingpdf +spec: + type: ClusterIP + selector: + app: stirlingpdf + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: http +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: stirlingpdf-config + namespace: default + annotations: + nfs.io/storage-path: "stirlingpdf-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: stirlingpdf + namespace: default + labels: + app: stirlingpdf +spec: + rules: + - host: stirling.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: stirlingpdf + port: + number: 8080 diff --git a/apps/default/uptimekuma.yaml b/apps/default/uptimekuma.yaml new file mode 100644 index 0000000..2b68364 --- /dev/null +++ b/apps/default/uptimekuma.yaml @@ -0,0 +1,106 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: uptimekuma + namespace: default +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app: uptimekuma + template: + metadata: + labels: + app: uptimekuma + spec: + containers: + - name: uptimekuma + image: louislam/uptime-kuma:2 + imagePullPolicy: Always + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + ports: + - containerPort: 3001 + name: uptimekuma-port + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: / + port: 3001 + initialDelaySeconds: 30 + periodSeconds: 60 + readinessProbe: + httpGet: + path: / + port: 3001 + initialDelaySeconds: 5 + periodSeconds: 5 + volumeMounts: + - name: uptimekuma-config + mountPath: /app/data + volumes: + - name: uptimekuma-config + persistentVolumeClaim: + claimName: uptimekuma-config +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: uptimekuma + namespace: default +spec: + type: ClusterIP + selector: + app: uptimekuma + ports: + - port: 3001 + targetPort: uptimekuma-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: uptimekuma-config + namespace: default + annotations: + nfs.io/storage-path: "uptimekuma-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: uptimekuma + namespace: default +spec: + rules: + - host: uptimekuma.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: uptimekuma + port: + number: 3001 diff --git a/apps/default/vaultwarden.yaml b/apps/default/vaultwarden.yaml new file mode 100644 index 0000000..f7162fc --- /dev/null +++ b/apps/default/vaultwarden.yaml @@ -0,0 +1,146 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vaultwarden + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: vaultwarden + template: + metadata: + labels: + app: vaultwarden + spec: + containers: + - name: vaultwarden + image: vaultwarden/server:latest + imagePullPolicy: Always + env: + - name: DOMAIN + value: "https://vault.haven" + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: vaultwarden-admin-token + key: ADMIN_TOKEN + ports: + - containerPort: 80 + name: vault-port + # /alive requires no authentication and verifies the Vaultwarden process + startupProbe: + httpGet: + path: /alive + port: vault-port + scheme: HTTP + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 30 + readinessProbe: + httpGet: + path: /alive + port: vault-port + scheme: HTTP + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /alive + port: vault-port + scheme: HTTP + periodSeconds: 15 + timeoutSeconds: 3 + failureThreshold: 6 + volumeMounts: + - name: vaultwarden-data + mountPath: /data + resources: + requests: + cpu: 250m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi + volumes: + - name: vaultwarden-data + persistentVolumeClaim: + claimName: vaultwarden-data +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: vaultwarden + namespace: default +spec: + type: ClusterIP + selector: + app: vaultwarden + ports: + - port: 80 + targetPort: vault-port +--- +# 3) PersistentVolumeClaim (for /data) +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: vaultwarden-data + namespace: default + annotations: + nfs.io/storage-path: "vaultwarden-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vaultwarden + namespace: default + annotations: + cert-manager.io/cluster-issuer: internal-ca + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" +spec: + tls: + - hosts: + - vault.haven + secretName: vaultwarden-tls + rules: + - host: vault.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vaultwarden + port: + number: 80 +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vaultwarden-public + namespace: default +spec: + rules: + - host: vault.ivanch.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vaultwarden + port: + number: 80 diff --git a/apps/dev/gitea-runner.yaml b/apps/dev/gitea-runner.yaml new file mode 100644 index 0000000..190826b --- /dev/null +++ b/apps/dev/gitea-runner.yaml @@ -0,0 +1,202 @@ +# --- ConfigMap for the AMD64 Runner --- +apiVersion: v1 +kind: ConfigMap +metadata: + name: gitea-runner-amd64-config + namespace: dev +data: + config.yaml: | + # Registration token and Gitea instance URL should be managed via secrets + runner: + capacity: 4 + timeout: 1h + labels: + - "ubuntu-amd64:docker://docker.gitea.com/runner-images:ubuntu-latest" + - "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest" + - "ubuntu-slim:docker://docker.gitea.com/runner-images:ubuntu-latest-slim" + - "runner-full:docker://git.ivanch.me/ivanch/runner-images:full" + - "runner-slim:docker://git.ivanch.me/ivanch/runner-images:slim" + - "runner-full-amd64:docker://git.ivanch.me/ivanch/runner-images:full" + - "runner-slim-amd64:docker://git.ivanch.me/ivanch/runner-images:slim" +--- +# --- ConfigMap for the ARM64 Runner --- +apiVersion: v1 +kind: ConfigMap +metadata: + name: gitea-runner-arm64-config + namespace: dev +data: + config.yaml: | + runner: + capacity: 4 + timeout: 1h + labels: + - "ubuntu-arm64:docker://docker.gitea.com/runner-images:ubuntu-latest" + - "runner-full-arm64:docker://git.ivanch.me/ivanch/runner-images:full" + - "runner-slim-arm64:docker://git.ivanch.me/ivanch/runner-images:slim" +--- +# PersistentVolumeClaim for AMD64 +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gitea-runner-amd64-pvc + namespace: dev + annotations: + nfs.io/storage-path: "gitea-runner-amd64-pvc" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 8Mi +--- +# PersistentVolumeClaim for ARM64 +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gitea-runner-arm64-pvc + namespace: dev + annotations: + nfs.io/storage-path: "gitea-runner-arm64-pvc" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 8Mi +--- +# --- Deployment for the AMD64 Runner --- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gitea-runner-amd64 + namespace: dev +spec: + replicas: 1 + selector: + matchLabels: + app: gitea-runner-amd64 + template: + metadata: + labels: + app: gitea-runner-amd64 + spec: + containers: + - name: gitea-runner + image: gitea/act_runner:latest + imagePullPolicy: Always + resources: + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "4000m" + memory: "4Gi" + volumeMounts: + - name: config-volume + mountPath: /etc/gitea-runner/config.yaml + subPath: config.yaml + - name: docker-socket + mountPath: /var/run/docker.sock + - name: gitea-runner-amd64-pvc + mountPath: /data + env: + - name: GITEA_RUNNER_REGISTRATION_TOKEN + valueFrom: + secretKeyRef: + name: gitea-runner-token + key: REGISTRATION_TOKEN + - name: GITEA_INSTANCE_URL + value: https://git.ivanch.me + - name: GITEA_RUNNER_NAME + value: k8s-runner-amd64 + - name: CONFIG_FILE + value: /etc/gitea-runner/config.yaml + volumes: + - name: config-volume + configMap: + name: gitea-runner-amd64-config + - name: docker-socket + hostPath: + path: /var/run/docker.sock + - name: gitea-runner-amd64-pvc + persistentVolumeClaim: + claimName: gitea-runner-amd64-pvc + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - iris +--- +# --- Deployment for the ARM64 Runner --- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gitea-runner-arm64 + namespace: dev +spec: + replicas: 1 + selector: + matchLabels: + app: gitea-runner-arm64 + template: + metadata: + labels: + app: gitea-runner-arm64 + spec: + containers: + - name: gitea-runner + image: gitea/act_runner:latest + imagePullPolicy: Always + resources: + requests: + cpu: "500m" + memory: "512Mi" + limits: + cpu: "4000m" + memory: "4Gi" + volumeMounts: + - name: config-volume + mountPath: /etc/gitea-runner/config.yaml + subPath: config.yaml + - name: docker-socket + mountPath: /var/run/docker.sock + - name: gitea-runner-arm64-pvc + mountPath: /data + env: + - name: GITEA_RUNNER_REGISTRATION_TOKEN + valueFrom: + secretKeyRef: + name: gitea-runner-token + key: REGISTRATION_TOKEN + - name: GITEA_INSTANCE_URL + value: https://git.ivanch.me + - name: GITEA_RUNNER_NAME + value: k8s-runner-arm64 + - name: CONFIG_FILE + value: /etc/gitea-runner/config.yaml + volumes: + - name: config-volume + configMap: + name: gitea-runner-arm64-config + - name: docker-socket + hostPath: + path: /var/run/docker.sock + - name: gitea-runner-arm64-pvc + persistentVolumeClaim: + claimName: gitea-runner-arm64-pvc + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - nexus diff --git a/apps/dns/adguard-sync.yaml b/apps/dns/adguard-sync.yaml new file mode 100644 index 0000000..fd9e792 --- /dev/null +++ b/apps/dns/adguard-sync.yaml @@ -0,0 +1,117 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: adguardsync-pvc + namespace: dns + annotations: + nfs.io/storage-path: "adguardsync-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Mi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: adguardsync + namespace: dns +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: adguardsync + template: + metadata: + labels: + app: adguardsync + spec: + containers: + - name: adguardsync + image: ghcr.io/bakito/adguardhome-sync:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + protocol: TCP + name: web-port + env: + - name: CRON + value: "0 * * * *" + - name: RUN_ON_START + value: "true" + - name: LOG_LEVEL + value: "info" + - name: ORIGIN_URL + value: "http://adguard.haven" + - name: ORIGIN_USERNAME + valueFrom: + secretKeyRef: + name: adguardhome-password + key: username + - name: ORIGIN_PASSWORD + valueFrom: + secretKeyRef: + name: adguardhome-password + key: password + - name: REPLICA1_URL + value: "http://adguard2.haven" + - name: REPLICA1_USERNAME + valueFrom: + secretKeyRef: + name: adguardhome-password + key: username + - name: REPLICA1_PASSWORD + valueFrom: + secretKeyRef: + name: adguardhome-password + key: password + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: adguardsync-storage + mountPath: /config + volumes: + - name: adguardsync-storage + persistentVolumeClaim: + claimName: adguardsync-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: adguardsync-svc + namespace: dns +spec: + type: ClusterIP + selector: + app: adguardsync + ports: + - name: web + port: 8080 + targetPort: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: adguardsync-ingress + namespace: dns +spec: + rules: + - host: adguardsync.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: adguardsync-svc + port: + number: 8080 diff --git a/apps/dns/adguard.yaml b/apps/dns/adguard.yaml new file mode 100644 index 0000000..e9f4aa0 --- /dev/null +++ b/apps/dns/adguard.yaml @@ -0,0 +1,171 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: adguardhome-pvc + namespace: dns + annotations: + nfs.io/storage-path: "adguardhome-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: adguardhome + namespace: dns +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: adguardhome + template: + metadata: + labels: + app: adguardhome + spec: + containers: + - name: adguardhome + image: adguard/adguardhome:latest + imagePullPolicy: Always + ports: + - containerPort: 53 + protocol: TCP + - containerPort: 53 + protocol: UDP + - containerPort: 3000 + protocol: TCP + name: install-port + - containerPort: 80 + protocol: TCP + name: web-port + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1000m + memory: 2Gi + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 5 + volumeMounts: + - name: adguardhome-storage + mountPath: /opt/adguardhome/work + - name: adguardhome-storage + mountPath: /opt/adguardhome/conf + volumes: + - name: adguardhome-storage + persistentVolumeClaim: + claimName: adguardhome-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: adguardhome-svc + namespace: dns +spec: + type: LoadBalancer + selector: + app: adguardhome + loadBalancerIP: 192.168.20.200 + ports: + - name: dns-tcp + port: 53 + targetPort: 53 + protocol: TCP + - name: dns-udp + port: 53 + targetPort: 53 + protocol: UDP + - name: web + port: 80 + targetPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: adguardhome-lan-svc + namespace: dns +spec: + type: LoadBalancer + selector: + app: adguardhome + loadBalancerIP: 192.168.15.200 + ports: + - name: dns-tcp + port: 53 + targetPort: 53 + protocol: TCP + - name: dns-udp + port: 53 + targetPort: 53 + protocol: UDP + - name: web + port: 80 + targetPort: 80 +--- +apiVersion: v1 +kind: Service +metadata: + name: adguard-install-svc + namespace: dns +spec: + type: ClusterIP + selector: + app: adguardhome + ports: + - name: install + port: 3000 + targetPort: 3000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: adguardhome-ingress + namespace: dns +spec: + rules: + - host: adguard.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: adguardhome-svc + port: + number: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: adguardhome-install-ingress + namespace: dns +spec: + rules: + - host: install.adguard.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: adguard-install-svc + port: + number: 3000 diff --git a/apps/infra/beszel-agent.yaml b/apps/infra/beszel-agent.yaml new file mode 100644 index 0000000..49043c7 --- /dev/null +++ b/apps/infra/beszel-agent.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: beszel-agent + namespace: infra +spec: + selector: + matchLabels: + app: beszel-agent + template: + metadata: + labels: + app: beszel-agent + spec: + hostNetwork: true + containers: + - env: + - name: PORT + value: "45876" + - name: KEY + valueFrom: + secretKeyRef: + name: beszel-key + key: SECRET-KEY + image: henrygd/beszel-agent:0.18.8 + imagePullPolicy: Always + name: beszel-agent + ports: + - containerPort: 45876 + hostPort: 45876 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "200m" + restartPolicy: Always diff --git a/apps/infra/beszel.yaml b/apps/infra/beszel.yaml new file mode 100644 index 0000000..19741d8 --- /dev/null +++ b/apps/infra/beszel.yaml @@ -0,0 +1,96 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: beszel + namespace: infra +spec: + replicas: 1 + selector: + matchLabels: + app: beszel + template: + metadata: + labels: + app: beszel + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/arch + operator: In + values: + - amd64 + containers: + - name: beszel + image: ghcr.io/henrygd/beszel/beszel:0.18.8 + imagePullPolicy: Always + ports: + - containerPort: 8090 + name: beszel-port + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumeMounts: + - name: beszel-config + mountPath: /beszel_data + volumes: + - name: beszel-config + persistentVolumeClaim: + claimName: beszel-config +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: beszel + namespace: infra +spec: + type: ClusterIP + selector: + app: beszel + ports: + - port: 80 + targetPort: beszel-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: beszel-config + namespace: infra + annotations: + nfs.io/storage-path: "beszel-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: beszel + namespace: infra +spec: + rules: + - host: beszel.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: beszel + port: + number: 80 diff --git a/apps/infra/bitwarden-cli.yaml b/apps/infra/bitwarden-cli.yaml new file mode 100644 index 0000000..f76c76f --- /dev/null +++ b/apps/infra/bitwarden-cli.yaml @@ -0,0 +1,144 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bitwarden-cli + namespace: infra + labels: + app.kubernetes.io/name: bitwarden-cli +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: bitwarden-cli + template: + metadata: + labels: + app.kubernetes.io/name: bitwarden-cli + spec: + nodeSelector: + kubernetes.io/arch: amd64 + containers: + - name: bitwarden-cli + image: ghcr.io/charlesthomas/bitwarden-cli:2026.7.0 + imagePullPolicy: IfNotPresent + # Override the baked entrypoint so --disable-origin-protection is + # actually passed (it was commented out in the image's entrypoint.sh, + # which made bw serve reject all cross-pod requests -> connection refused). + command: ["/bin/bash", "-lc"] + args: + - | + set -e + bw config server "${BW_HOST}" + + # Authenticate. Prefer the API key if client creds are present. + login() { + if [ -n "$BW_CLIENTID" ] && [ -n "$BW_CLIENTSECRET" ]; then + echo "Using apikey to log in" + BW_SESSION=$(bw login --apikey --raw) || return 1 + else + echo "Using password to log in" + BW_SESSION=$(bw login "${BW_USER}" --passwordenv BW_PASSWORD --raw) || return 1 + fi + export BW_SESSION + } + + login + + # Warm the vault cache once at startup so bw serve has data immediately. + bw sync + bw status + + # Keep the session alive + + echo "Starting periodic bw login+sync loop (every 5m)" + ( + while true; do + sleep 300 + login || true + echo "[$(date -u +%FT%TZ)] bw sync" + bw sync >/dev/null 2>&1 || echo "[$(date -u +%FT%TZ)] bw sync failed" + done + ) & + + echo 'Running `bw serve` on port 8087' + bw serve --hostname 0.0.0.0 --disable-origin-protection + env: + - name: BW_HOST + valueFrom: + secretKeyRef: + name: bitwarden-cli + key: BW_HOST + - name: BW_USER + valueFrom: + secretKeyRef: + name: bitwarden-cli + key: BW_USERNAME + - name: BW_PASSWORD + valueFrom: + secretKeyRef: + name: bitwarden-cli + key: BW_PASSWORD + ports: + - name: http + containerPort: 8087 + protocol: TCP + startupProbe: + tcpSocket: { port: 8087 } + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 30 + readinessProbe: + tcpSocket: { port: 8087 } + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + tcpSocket: { port: 8087 } + periodSeconds: 15 + timeoutSeconds: 3 + failureThreshold: 6 + resources: + limits: + cpu: 400m + memory: 512Mi + requests: + cpu: 50m + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: bitwarden-cli + namespace: infra + labels: + app.kubernetes.io/name: bitwarden-cli +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: bitwarden-cli + ports: + - name: http + port: 8087 + targetPort: http + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: bw-cli + namespace: infra +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: bitwarden-cli + policyTypes: + - Ingress + ingress: + - from: + # ESO pods in the external-secrets namespace. + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: external-secrets + podSelector: + matchLabels: + app.kubernetes.io/name: external-secrets diff --git a/apps/infra/code-config.yaml b/apps/infra/code-config.yaml new file mode 100644 index 0000000..953cc61 --- /dev/null +++ b/apps/infra/code-config.yaml @@ -0,0 +1,112 @@ +--- +# 1) Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: code-config + namespace: infra +spec: + replicas: 1 + selector: + matchLabels: + app: code-config + template: + metadata: + labels: + app: code-config + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - iris + containers: + - name: code-config + image: lscr.io/linuxserver/code-server:latest + imagePullPolicy: Always + env: + - name: PUID + value: "1000" + - name: PGID + value: "1000" + - name: PROXY_DOMAIN + value: "code-config.haven" + - name: DEFAULT_WORKSPACE + value: "/k8s-config" + resources: + requests: + memory: 512Mi + cpu: 200m + limits: + memory: 1Gi + cpu: 2000m + ports: + - containerPort: 8443 + name: code-port + volumeMounts: + - name: code-config + mountPath: /config + - name: k8s-config + mountPath: /k8s-config + volumes: + - name: code-config + persistentVolumeClaim: + claimName: code-config + - name: k8s-config + nfs: + server: nfs-config.haven + path: /export/config +--- +# 2) Service +apiVersion: v1 +kind: Service +metadata: + name: code-config + namespace: infra +spec: + type: ClusterIP + selector: + app: code-config + ports: + - port: 8443 + targetPort: code-port +--- +# 3) PersistentVolumeClaim +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: code-config + namespace: infra + annotations: + nfs.io/storage-path: "code-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- +# 4) Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: code-config + namespace: infra +spec: + rules: + - host: code-config.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: code-config + port: + number: 8443 diff --git a/apps/infra/csi-driver-nfs.yaml b/apps/infra/csi-driver-nfs.yaml new file mode 100644 index 0000000..35941bd --- /dev/null +++ b/apps/infra/csi-driver-nfs.yaml @@ -0,0 +1,26 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: csi-driver-nfs + namespace: infra +spec: + repo: https://kubernetes-csi.github.io/csi-driver-nfs + chart: csi-driver-nfs + version: 4.13.4 + targetNamespace: infra + valuesContent: |- + controller: + replicas: 1 + logLevel: 5 + defaultOnDeletePolicy: retain + storageClasses: + - name: nfs-client + annotations: + storageclass.kubernetes.io/is-default-class: "true" + parameters: + server: nfs-config.haven + share: /export/config + subDir: ${pvc.metadata.namespace}/${pvc.metadata.name} + onDelete: retain + reclaimPolicy: Retain + volumeBindingMode: Immediate diff --git a/apps/infra/external-secrets-helm.yaml b/apps/infra/external-secrets-helm.yaml new file mode 100644 index 0000000..c017c87 --- /dev/null +++ b/apps/infra/external-secrets-helm.yaml @@ -0,0 +1,11 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: external-secrets + namespace: kube-system +spec: + repo: https://charts.external-secrets.io + chart: external-secrets + version: 2.7.0 + targetNamespace: external-secrets + createNamespace: true diff --git a/apps/infra/file-nginx.yaml b/apps/infra/file-nginx.yaml new file mode 100644 index 0000000..c5b5121 --- /dev/null +++ b/apps/infra/file-nginx.yaml @@ -0,0 +1,128 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: file-nginx + namespace: infra + labels: + app: file-nginx +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + selector: + matchLabels: + app: file-nginx + template: + metadata: + labels: + app: file-nginx + spec: + containers: + - name: nginx + image: nginx:alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + securityContext: + allowPrivilegeEscalation: false + runAsUser: 0 + volumeMounts: + - name: html + mountPath: /usr/share/nginx/data + - name: nginx-conf + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + startupProbe: + httpGet: + path: / + port: 80 + failureThreshold: 30 + periodSeconds: 5 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + volumes: + - name: html + nfs: + server: vega.haven + path: /export/Fast + - name: nginx-conf + configMap: + name: file-nginx-conf + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: file-nginx-conf + namespace: infra +data: + default.conf: | + server { + listen 80; + listen [::]:80; + server_name _; + + root /usr/share/nginx/data/file-nginx; + index index.html; + + autoindex on; + + location / { + try_files $uri $uri/ =404; + } + } +--- +apiVersion: v1 +kind: Service +metadata: + name: file-nginx + namespace: infra +spec: + selector: + app: file-nginx + ports: + - protocol: TCP + port: 80 + targetPort: 80 + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: file-nginx + namespace: infra + annotations: + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "*" +spec: + rules: + - host: file-nginx.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: file-nginx + port: + number: 80 diff --git a/apps/infra/haven-notify.yaml b/apps/infra/haven-notify.yaml new file mode 100644 index 0000000..817f0b9 --- /dev/null +++ b/apps/infra/haven-notify.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: haven-notify + namespace: infra + labels: + app: haven-notify +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + selector: + matchLabels: + app: haven-notify + template: + metadata: + labels: + app: haven-notify + spec: + containers: + - name: haven-notify + image: git.ivanch.me/ivanch/haven-notify:latest + imagePullPolicy: Always + ports: + - containerPort: 8080 + env: + - name: WEBHOOK_URL + valueFrom: + secretKeyRef: + name: discord-webhook + key: HAVEN_WEBHOOK_URL + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /live + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +--- +apiVersion: v1 +kind: Service +metadata: + name: haven-notify + namespace: infra +spec: + selector: + app: haven-notify + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: haven-notify + namespace: infra +spec: + rules: + - host: notify.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: haven-notify + port: + number: 8080 diff --git a/apps/infra/ingress-traefik.yaml b/apps/infra/ingress-traefik.yaml new file mode 100644 index 0000000..902f92b --- /dev/null +++ b/apps/infra/ingress-traefik.yaml @@ -0,0 +1,32 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: traefik + namespace: kube-system +spec: + repo: https://traefik.github.io/charts + chart: traefik + version: 41.2.0 + targetNamespace: traefik + createNamespace: true + valuesContent: |- + deployment: + replicas: 2 + ingressClass: + enabled: true + isDefaultClass: true + name: traefik + service: + annotations: + metallb.io/ip-allocated-from-pool: default-pool + metallb.io/loadBalancerIPs: "192.168.20.204" + spec: + type: LoadBalancer + externalTrafficPolicy: Local + resources: + requests: + cpu: 100m + memory: 90Mi + limits: + cpu: 1000m + memory: 256Mi diff --git a/apps/infra/wg-easy.yaml b/apps/infra/wg-easy.yaml new file mode 100644 index 0000000..53bdbc4 --- /dev/null +++ b/apps/infra/wg-easy.yaml @@ -0,0 +1,126 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: wg-easy-pvc + namespace: infra + annotations: + nfs.io/storage-path: "wg-easy-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wg-easy + namespace: infra +spec: + strategy: + type: Recreate + replicas: 1 + selector: + matchLabels: + app: wg-easy + template: + metadata: + labels: + app: wg-easy + spec: + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - preference: + matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - nexus + weight: 100 + containers: + - name: wg-easy + image: ghcr.io/wg-easy/wg-easy:latest + imagePullPolicy: Always + ports: + - containerPort: 51820 + protocol: UDP + name: wg-port + - containerPort: 51821 + protocol: TCP + name: web-port + env: + - name: LANG + value: en + - name: WG_HOST + value: vpn.ivanch.me + - name: WG_MTU + value: "1420" + - name: UI_TRAFFIC_STATS + value: "true" + - name: UI_CHART_TYPE + value: "0" + - name: WG_ENABLE_ONE_TIME_LINKS + value: "true" + - name: UI_ENABLE_SORT_CLIENTS + value: "true" + securityContext: + capabilities: + add: + - NET_ADMIN + - SYS_MODULE + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 2000m + memory: 1Gi + volumeMounts: + - name: wg-easy-volume + mountPath: /etc/wireguard + restartPolicy: Always + volumes: + - name: wg-easy-volume + persistentVolumeClaim: + claimName: wg-easy-pvc +--- +apiVersion: v1 +kind: Service +metadata: + name: wg-easy-svc + namespace: infra +spec: + type: LoadBalancer + selector: + app: wg-easy + loadBalancerIP: 192.168.20.203 + ports: + - name: wg-port + port: 51820 + targetPort: 51820 + protocol: UDP + - name: web-port + port: 51821 + targetPort: 51821 + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: wg-easy-ingress + namespace: infra +spec: + rules: + - host: vpn.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: wg-easy-svc + port: + number: 51821 diff --git a/apps/metallb-system/metallb.yaml b/apps/metallb-system/metallb.yaml new file mode 100644 index 0000000..dea9e71 --- /dev/null +++ b/apps/metallb-system/metallb.yaml @@ -0,0 +1,10 @@ +apiVersion: helm.cattle.io/v1 +kind: HelmChart +metadata: + name: metallb + namespace: kube-system +spec: + repo: https://metallb.github.io/metallb + chart: metallb + version: 0.15.2 + targetNamespace: metallb-system diff --git a/apps/monitoring/grafana.yaml b/apps/monitoring/grafana.yaml new file mode 100644 index 0000000..ea40eb6 --- /dev/null +++ b/apps/monitoring/grafana.yaml @@ -0,0 +1,125 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: grafana + name: grafana + namespace: monitoring +spec: + selector: + matchLabels: + app: grafana + template: + metadata: + labels: + app: grafana + spec: + securityContext: + fsGroup: 472 + supplementalGroups: + - 0 + containers: + - name: grafana + image: grafana/grafana:latest + imagePullPolicy: Always + ports: + - containerPort: 3000 + name: http-grafana + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /robots.txt + port: 3000 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 2 + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 10 + successThreshold: 1 + tcpSocket: + port: 3000 + timeoutSeconds: 1 + resources: + requests: + cpu: 250m + memory: 750Mi + limits: + memory: 1Gi + cpu: 500m + volumeMounts: + - mountPath: /var/lib/grafana + name: grafana-pv + volumes: + - name: grafana-pv + persistentVolumeClaim: + claimName: grafana-pvc +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: grafana-pvc + namespace: monitoring + annotations: + nfs.io/storage-path: "grafana-data" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + namespace: monitoring + name: grafana +spec: + ports: + - port: 3000 + protocol: TCP + targetPort: http-grafana + selector: + app: grafana + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + namespace: monitoring + name: grafana +spec: + rules: + - host: grafana.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: grafana + port: + number: 3000 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + namespace: monitoring + name: grafana-public +spec: + rules: + - host: grafanah.ivanch.me + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: grafana + port: + number: 3000 diff --git a/apps/monitoring/kube-state-metrics.yaml b/apps/monitoring/kube-state-metrics.yaml new file mode 100644 index 0000000..75d2945 --- /dev/null +++ b/apps/monitoring/kube-state-metrics.yaml @@ -0,0 +1,145 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kube-state-metrics + namespace: monitoring + labels: + app: kube-state-metrics + app.kubernetes.io/component: exporter + app.kubernetes.io/name: kube-state-metrics + app.kubernetes.io/version: 2.19.1 +automountServiceAccountToken: false +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kube-state-metrics + labels: + app: kube-state-metrics + app.kubernetes.io/component: exporter + app.kubernetes.io/name: kube-state-metrics + app.kubernetes.io/version: 2.19.1 +rules: + - apiGroups: [""] + resources: + - nodes + - pods + - persistentvolumeclaims + verbs: ["list", "watch"] + - apiGroups: ["apps"] + resources: + - statefulsets + - daemonsets + - deployments + - replicasets + verbs: ["list", "watch"] + - apiGroups: ["batch"] + resources: + - cronjobs + - jobs + verbs: ["list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kube-state-metrics + labels: + app: kube-state-metrics + app.kubernetes.io/component: exporter + app.kubernetes.io/name: kube-state-metrics + app.kubernetes.io/version: 2.19.1 +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kube-state-metrics +subjects: + - kind: ServiceAccount + name: kube-state-metrics + namespace: monitoring +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kube-state-metrics + namespace: monitoring + labels: + app: kube-state-metrics + app.kubernetes.io/component: exporter + app.kubernetes.io/name: kube-state-metrics + app.kubernetes.io/version: 2.19.1 +spec: + replicas: 1 + selector: + matchLabels: + app: kube-state-metrics + template: + metadata: + labels: + app: kube-state-metrics + app.kubernetes.io/component: exporter + app.kubernetes.io/name: kube-state-metrics + app.kubernetes.io/version: 2.19.1 + spec: + automountServiceAccountToken: true + serviceAccountName: kube-state-metrics + nodeSelector: + kubernetes.io/os: linux + containers: + - name: kube-state-metrics + image: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.19.1 + imagePullPolicy: IfNotPresent + args: + - --resources=cronjobs,daemonsets,deployments,jobs,nodes,persistentvolumeclaims,pods,replicasets,statefulsets + ports: + - name: http-metrics + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: /livez + port: http-metrics + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + timeoutSeconds: 5 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 65534 + seccompProfile: + type: RuntimeDefault + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: kube-state-metrics + namespace: monitoring + labels: + app: kube-state-metrics + app.kubernetes.io/component: exporter + app.kubernetes.io/name: kube-state-metrics + app.kubernetes.io/version: 2.19.1 +spec: + type: ClusterIP + selector: + app: kube-state-metrics + ports: + - name: http-metrics + port: 8080 + targetPort: http-metrics + protocol: TCP diff --git a/apps/monitoring/loki.yaml b/apps/monitoring/loki.yaml new file mode 100644 index 0000000..952a937 --- /dev/null +++ b/apps/monitoring/loki.yaml @@ -0,0 +1,108 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: loki + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: loki + template: + metadata: + labels: + app: loki + spec: + containers: + - name: loki + image: grafana/loki:3 + args: ["-config.file=/etc/loki/config/config.yaml"] + ports: + - containerPort: 3100 + volumeMounts: + - name: config + mountPath: /etc/loki/config + - name: loki-storage + mountPath: /tmp/loki + resources: + requests: + cpu: 100m + memory: 1Gi + limits: + cpu: 200m + memory: 1Gi + volumes: + - name: config + configMap: + name: loki-config + - name: loki-storage + emptyDir: + medium: Memory +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: loki-config + namespace: monitoring +data: + config.yaml: | + auth_enabled: true + server: + http_listen_port: 3100 + + common: + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + replication_factor: 1 + path_prefix: /tmp/loki + querier: + multi_tenant_queries_enabled: true + + schema_config: + configs: + - from: "2024-01-01" + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + + storage_config: + tsdb_shipper: + active_index_directory: /tmp/loki/index + cache_location: /tmp/loki/cache + filesystem: + directory: /tmp/loki/chunks + + limits_config: + allow_structured_metadata: true + retention_period: 0 + + ingester: + lifecycler: + ring: + kvstore: + store: inmemory + replication_factor: 1 + chunk_idle_period: 1m + max_chunk_age: 5m + chunk_target_size: 1536000 + + compactor: + retention_enabled: false +--- +apiVersion: v1 +kind: Service +metadata: + name: loki + namespace: monitoring +spec: + ports: + - port: 3100 + targetPort: 3100 + name: http + selector: + app: loki diff --git a/apps/monitoring/nodeexporter.yaml b/apps/monitoring/nodeexporter.yaml new file mode 100644 index 0000000..bac150b --- /dev/null +++ b/apps/monitoring/nodeexporter.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: node-exporter + namespace: monitoring + labels: + app: node-exporter +spec: + selector: + matchLabels: + app: node-exporter + template: + metadata: + labels: + app: node-exporter + spec: + hostNetwork: true + containers: + - name: node-exporter + image: prom/node-exporter:latest + imagePullPolicy: Always + args: + - "--path.rootfs=/host" + ports: + - containerPort: 9100 + hostPort: 9100 + name: metrics + protocol: TCP + resources: + requests: + memory: "50Mi" + cpu: "100m" + limits: + memory: "100Mi" + cpu: "200m" + volumeMounts: + - name: host + mountPath: /host + readOnly: true + volumes: + - name: host + hostPath: + path: / +--- +apiVersion: v1 +kind: Service +metadata: + name: node-exporter + namespace: monitoring +spec: + selector: + app: node-exporter + ports: + - name: metrics + port: 9100 + targetPort: metrics diff --git a/apps/monitoring/prometheus.yaml b/apps/monitoring/prometheus.yaml new file mode 100644 index 0000000..b85a9e0 --- /dev/null +++ b/apps/monitoring/prometheus.yaml @@ -0,0 +1,130 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus + namespace: monitoring + labels: + app: prometheus +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + serviceAccountName: prometheus + containers: + - name: prometheus + image: prom/prometheus:latest + args: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--storage.tsdb.retention.time=1d" + - "--web.enable-lifecycle" + ports: + - containerPort: 9090 + name: web + volumeMounts: + - name: prometheus-config-volume + mountPath: /etc/prometheus + - name: prometheus-storage + mountPath: /prometheus + resources: + requests: + memory: "500Mi" + cpu: "200m" + limits: + memory: "1Gi" + cpu: "500m" + volumes: + - name: prometheus-config-volume + persistentVolumeClaim: + claimName: prometheus-pvc + - name: prometheus-storage + emptyDir: + medium: Memory + sizeLimit: 256Mi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: prometheus-pvc + namespace: monitoring + annotations: + nfs.io/storage-path: "prometheus-config" +spec: + storageClassName: "nfs-client" + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +# Service URL - http://prometheus.monitoring.svc.cluster.local:9090 +apiVersion: v1 +kind: Service +metadata: + name: prometheus + namespace: monitoring + labels: + app: prometheus +spec: + ports: + - name: web + port: 9090 + targetPort: web + selector: + app: prometheus + type: ClusterIP +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prometheus + namespace: monitoring + labels: + app: prometheus +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: prometheus + namespace: monitoring + labels: + app: prometheus +rules: +- apiGroups: [""] + resources: + - nodes + - nodes/proxy + - services + - endpoints + - pods + verbs: ["get", "list", "watch"] +- apiGroups: ["extensions"] + resources: + - ingresses + verbs: ["get", "list", "watch"] +- apiGroups: ["networking.k8s.io"] + resources: + - ingresses + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: prometheus + namespace: monitoring + labels: + app: prometheus +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: prometheus +subjects: +- kind: ServiceAccount + name: prometheus + namespace: monitoring \ No newline at end of file diff --git a/apps/root/applicationset.yaml b/apps/root/applicationset.yaml new file mode 100644 index 0000000..f52e48d --- /dev/null +++ b/apps/root/applicationset.yaml @@ -0,0 +1,46 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: haven-apps + namespace: argocd +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - git: + repoURL: https://git.ivanch.me/ivanch/haven.git + revision: main + files: + - path: "apps/*/*.yaml" + - path: "apps/root/*.yaml" + exclude: true + template: + metadata: + name: '{{ .path.filename | trimSuffix ".yaml" }}' + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io + spec: + project: default + source: + repoURL: https://git.ivanch.me/ivanch/haven.git + targetRevision: main + path: "{{ .path.path }}" + directory: + include: "{{ .path.filename }}" + destination: + server: https://kubernetes.default.svc + namespace: "{{ index .path.segments 1 }}" + syncPolicy: + automated: + prune: true + selfHeal: true + templatePatch: | + {{- if hasKey . "chart" }} + spec: + source: + $patch: replace + {{- toYaml .spec.source | nindent 4 }} + syncPolicy: + syncOptions: [CreateNamespace=true, ServerSideApply=true] + {{- end }} diff --git a/apps/root/kustomization.yaml b/apps/root/kustomization.yaml new file mode 100644 index 0000000..7a2dffe --- /dev/null +++ b/apps/root/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - applicationset.yaml diff --git a/bootstrap/address-pool.yaml b/bootstrap/address-pool.yaml new file mode 100644 index 0000000..f3137ed --- /dev/null +++ b/bootstrap/address-pool.yaml @@ -0,0 +1,22 @@ +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: default-pool + namespace: metallb-system +spec: + addresses: + - 192.168.15.200/32 # reserved for DNS 1 + - 192.168.15.202/32 # reserved for wg-easy + - 192.168.15.203/32 # reserved for k3s ingress + - 192.168.20.200/32 # reserved for DNS 1 + # - 192.168.20.201 is reserved for DNS 2 + - 192.168.20.202-192.168.20.220 +--- +apiVersion: metallb.io/v1beta1 +kind: L2Advertisement +metadata: + name: default-advertisement + namespace: metallb-system +spec: + ipAddressPools: + - default-pool diff --git a/bootstrap/argocd-install/argocd-api-user.yaml b/bootstrap/argocd-install/argocd-api-user.yaml new file mode 100644 index 0000000..1f783a8 --- /dev/null +++ b/bootstrap/argocd-install/argocd-api-user.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cm + namespace: argocd + labels: + app.kubernetes.io/name: argocd-cm + app.kubernetes.io/part-of: argocd +data: + accounts.api: apiKey + accounts.api.enabled: "true" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-rbac-cm + namespace: argocd + labels: + app.kubernetes.io/name: argocd-rbac-cm + app.kubernetes.io/part-of: argocd +data: + policy.csv: | + g, api, role:readonly diff --git a/bootstrap/argocd-install/ingress.yaml b/bootstrap/argocd-install/ingress.yaml new file mode 100644 index 0000000..4d989e0 --- /dev/null +++ b/bootstrap/argocd-install/ingress.yaml @@ -0,0 +1,17 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: argocd-server + namespace: argocd +spec: + rules: + - host: argocd.haven + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: argocd-server + port: + number: 80 diff --git a/bootstrap/argocd-install/kustomization.yaml b/bootstrap/argocd-install/kustomization.yaml new file mode 100644 index 0000000..22548a0 --- /dev/null +++ b/bootstrap/argocd-install/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: argocd + +resources: + - https://github.com/argoproj/argo-cd.git/manifests/cluster-install?ref=stable + - ingress.yaml + +patches: + - path: server-insecure-patch.yaml + - path: argocd-api-user.yaml diff --git a/bootstrap/argocd-install/server-insecure-patch.yaml b/bootstrap/argocd-install/server-insecure-patch.yaml new file mode 100644 index 0000000..b9bebea --- /dev/null +++ b/bootstrap/argocd-install/server-insecure-patch.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cmd-params-cm +data: + server.insecure: "true" diff --git a/bootstrap/namespaces.yaml b/bootstrap/namespaces.yaml new file mode 100644 index 0000000..9a234e5 --- /dev/null +++ b/bootstrap/namespaces.yaml @@ -0,0 +1,141 @@ +--- +# ============================================================================== +# Core / System Namespaces +# ============================================================================== +apiVersion: v1 +kind: Namespace +metadata: + name: default +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kube-node-lease +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kube-public +--- +apiVersion: v1 +kind: Namespace +metadata: + name: kube-system +--- +# ============================================================================== +# Infrastructure, Ingress & GitOps +# ============================================================================== +apiVersion: v1 +kind: Namespace +metadata: + name: argocd +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cert-manager +--- +apiVersion: v1 +kind: Namespace +metadata: + name: dns +--- +apiVersion: v1 +kind: Namespace +metadata: + name: docker-ingress +--- +apiVersion: v1 +kind: Namespace +metadata: + name: external-secrets + labels: + name: external-secrets +--- +apiVersion: v1 +kind: Namespace +metadata: + name: infra +--- +apiVersion: v1 +kind: Namespace +metadata: + name: ingress-nginx + labels: + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/name: ingress-nginx +--- +apiVersion: v1 +kind: Namespace +metadata: + name: metallb-system +--- +# ============================================================================== +# Observability & Monitoring +# ============================================================================== +apiVersion: v1 +kind: Namespace +metadata: + name: alloy +--- +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring +--- +# ============================================================================== +# Storage & Data +# ============================================================================== +apiVersion: v1 +kind: Namespace +metadata: + name: garage + labels: + name: garage +--- +# ============================================================================== +# Environments & Workloads +# ============================================================================== +apiVersion: v1 +kind: Namespace +metadata: + name: chacal +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cloud +--- +apiVersion: v1 +kind: Namespace +metadata: + name: cronjobs +--- +apiVersion: v1 +kind: Namespace +metadata: + name: dev +--- +apiVersion: v1 +kind: Namespace +metadata: + name: lab +--- +apiVersion: v1 +kind: Namespace +metadata: + name: media +--- +apiVersion: v1 +kind: Namespace +metadata: + name: mindforge +--- +apiVersion: v1 +kind: Namespace +metadata: + name: vpn-session-pods + labels: + pod-security.kubernetes.io/audit: privileged + pod-security.kubernetes.io/enforce: privileged + pod-security.kubernetes.io/warn: privileged diff --git a/bootstrap/root-app.yaml b/bootstrap/root-app.yaml new file mode 100644 index 0000000..7245221 --- /dev/null +++ b/bootstrap/root-app.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: root + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://git.ivanch.me/ivanch/haven.git + targetRevision: main + path: apps/root + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true diff --git a/bootstrap/secretstores.yaml b/bootstrap/secretstores.yaml new file mode 100644 index 0000000..40c0d60 --- /dev/null +++ b/bootstrap/secretstores.yaml @@ -0,0 +1,20 @@ +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: { name: bitwarden-login } +spec: + provider: + webhook: + url: "http://bitwarden-cli.infra.svc:8087/object/item/{{ .remoteRef.key }}" + result: { jsonPath: "$.data.login.{{ .remoteRef.property }}" } +--- +apiVersion: external-secrets.io/v1 +kind: ClusterSecretStore +metadata: { name: bitwarden-fields } +spec: + provider: + webhook: + url: "http://bitwarden-cli.infra.svc:8087/object/item/{{ .remoteRef.key }}" + result: + { + jsonPath: '$.data.fields[?@.name=="{{ .remoteRef.property }}"].value', + }