From 8b81c090f6b984f88470710f353e947df9f81fb9 Mon Sep 17 00:00:00 2001 From: Ryan Blundon Date: Tue, 6 Oct 2026 13:49:13 -0500 Subject: [PATCH] obsidian deployment --- .../obsidian-vault/application.yaml | 24 +++++ .../obsidian-vault/deployment.yaml | 94 +++++++++++++++++++ .../obsidian-vault/image/Dockerfile | 9 ++ .../obsidian-vault/image/entrypoint.sh | 35 +++++++ .../obsidian-vault/kustomization.yaml | 8 ++ .../obsidian-vault/namespace.yaml | 9 ++ .../obsidian-vault/networkpolicy.yaml | 33 +++++++ .../obsidian-vault/obsidian-sync.yaml | 18 ++++ cluster/applications/obsidian-vault/pvcs.yaml | 29 ++++++ 9 files changed, 259 insertions(+) create mode 100644 cluster/applications/obsidian-vault/application.yaml create mode 100644 cluster/applications/obsidian-vault/deployment.yaml create mode 100644 cluster/applications/obsidian-vault/image/Dockerfile create mode 100644 cluster/applications/obsidian-vault/image/entrypoint.sh create mode 100644 cluster/applications/obsidian-vault/kustomization.yaml create mode 100644 cluster/applications/obsidian-vault/namespace.yaml create mode 100644 cluster/applications/obsidian-vault/networkpolicy.yaml create mode 100644 cluster/applications/obsidian-vault/obsidian-sync.yaml create mode 100644 cluster/applications/obsidian-vault/pvcs.yaml diff --git a/cluster/applications/obsidian-vault/application.yaml b/cluster/applications/obsidian-vault/application.yaml new file mode 100644 index 0000000..be811cd --- /dev/null +++ b/cluster/applications/obsidian-vault/application.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: obsidian-vault + namespace: argocd +spec: + project: obsidian + source: + repoURL: https://gitea.mk-labs.cloud/rblundon/homelab.git + targetRevision: main + path: deploy + directory: + # Exclude the Application manifest itself (already in argocd) + exclude: "application.yaml" + destination: + server: https://kubernetes.default.svc + namespace: obsidian + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - ServerSideApply=true + - PruneLast=true diff --git a/cluster/applications/obsidian-vault/deployment.yaml b/cluster/applications/obsidian-vault/deployment.yaml new file mode 100644 index 0000000..5764c8a --- /dev/null +++ b/cluster/applications/obsidian-vault/deployment.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: obsidian-vault + labels: + app.kubernetes.io/name: obsidian-vault + annotations: + argocd.argoproj.io/sync-wave: "8" +spec: + replicas: 1 # exactly one sync "device" per vault copy + strategy: + type: Recreate # never two sync clients on the same folder + selector: + matchLabels: + app.kubernetes.io/name: obsidian-vault + template: + metadata: + labels: + app.kubernetes.io/name: obsidian-vault + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + runAsUser: 10000 + runAsGroup: 10000 + fsGroup: 10000 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + containers: + - name: ob + image: ghcr.io/CHANGE_ME/obsidian-headless:0.0.13 # build from ./image + env: + - name: SYNC_MODE # mirror-remote = download only, revert local changes + value: mirror-remote + - name: DEBUG # "1" = idle instead of syncing, then kubectl exec in + value: "0" + - name: VAULT_PATH + value: /vault + - name: DEVICE_NAME # shows up in Obsidian's sync version history + value: k8s-obsidian + - name: EXCLUDED_FOLDERS # comma-separated folders that must never reach the cluster + value: "" + - name: HOME + value: /state + - name: XDG_CONFIG_HOME + value: /state/config + - name: VAULT_NAME + valueFrom: + secretKeyRef: + name: obsidian-vault + key: vault_name + - name: E2EE_PASSWORD + valueFrom: + secretKeyRef: + name: obsidian-vault + key: e2ee_password + optional: true + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 512Mi + volumeMounts: + - name: vault + mountPath: /vault + - name: state + mountPath: /state + - name: auth + mountPath: /secrets + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: vault + persistentVolumeClaim: + claimName: obsidian-vault + - name: state + persistentVolumeClaim: + claimName: obsidian-sync-state + - name: auth + secret: + secretName: obsidian-sync + items: + - key: auth_token + path: auth_token + - name: tmp + emptyDir: {} diff --git a/cluster/applications/obsidian-vault/image/Dockerfile b/cluster/applications/obsidian-vault/image/Dockerfile new file mode 100644 index 0000000..a0dddcc --- /dev/null +++ b/cluster/applications/obsidian-vault/image/Dockerfile @@ -0,0 +1,9 @@ +# No official image exists. Pin the npm version: `npm view obsidian-headless version` +FROM node:22-bookworm-slim +ARG OBSIDIAN_HEADLESS_VERSION=0.0.13 +RUN npm install -g obsidian-headless@${OBSIDIAN_HEADLESS_VERSION} \ + && useradd -u 10000 -m -d /state ob +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod 0755 /usr/local/bin/entrypoint.sh +USER 10000 +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/cluster/applications/obsidian-vault/image/entrypoint.sh b/cluster/applications/obsidian-vault/image/entrypoint.sh new file mode 100644 index 0000000..8560ed4 --- /dev/null +++ b/cluster/applications/obsidian-vault/image/entrypoint.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -eu +: "${VAULT_PATH:=/vault}" +: "${SYNC_MODE:=mirror-remote}" +: "${DEVICE_NAME:=k8s-obsidian}" +: "${VAULT_NAME:?VAULT_NAME is required}" + +CFG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/obsidian-headless" +mkdir -p "$CFG_DIR" +install -m 0600 /secrets/auth_token "$CFG_DIR/auth_token" + +# Debug mode: token is in place, but don't sync. Exec in and run `ob ...` by hand. +if [ "${DEBUG:-0}" = "1" ]; then + echo "DEBUG=1: idling. kubectl exec -it deploy/obsidian-sync -- sh" + exec sleep infinity +fi + +# Link the folder to the remote vault once. UNTESTED assumption: sync-status exits +# non-zero when the folder isn't configured yet; verify on your obsidian-headless version. +if ! ob sync-status --path "$VAULT_PATH" >/dev/null 2>&1; then + set -- --vault "$VAULT_NAME" --path "$VAULT_PATH" --device-name "$DEVICE_NAME" + if [ -n "${E2EE_PASSWORD:-}" ]; then + set -- "$@" --password "$E2EE_PASSWORD" + fi + ob sync-setup "$@" +fi + +# --configs "" = don't sync .obsidian app/plugin settings from this device +ob sync-config --path "$VAULT_PATH" \ + --mode "$SYNC_MODE" \ + --conflict-strategy merge \ + --configs "" \ + --excluded-folders "${EXCLUDED_FOLDERS:-}" + +exec ob sync --path "$VAULT_PATH" --continuous diff --git a/cluster/applications/obsidian-vault/kustomization.yaml b/cluster/applications/obsidian-vault/kustomization.yaml new file mode 100644 index 0000000..e981ee4 --- /dev/null +++ b/cluster/applications/obsidian-vault/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: obsidian +resources: + - namespace.yaml + - pvcs.yaml + - deployment.yaml + - networkpolicy.yaml diff --git a/cluster/applications/obsidian-vault/namespace.yaml b/cluster/applications/obsidian-vault/namespace.yaml new file mode 100644 index 0000000..89a357d --- /dev/null +++ b/cluster/applications/obsidian-vault/namespace.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: obsidian + labels: + # The sync pod is fully compliant with the "restricted" Pod Security Standard. + pod-security.kubernetes.io/enforce: restricted + annotations: + argocd.argoproj.io/sync-wave: "-1" diff --git a/cluster/applications/obsidian-vault/networkpolicy.yaml b/cluster/applications/obsidian-vault/networkpolicy.yaml new file mode 100644 index 0000000..f7b1d14 --- /dev/null +++ b/cluster/applications/obsidian-vault/networkpolicy.yaml @@ -0,0 +1,33 @@ +# Requires a CNI that enforces NetworkPolicy. Nothing may call the pod; it may only +# resolve DNS and reach the Obsidian Sync servers over HTTPS/WebSocket. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: obsidian-vault +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: obsidian-vault + policyTypes: ["Ingress", "Egress"] + ingress: [] + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - { protocol: UDP, port: 53 } + - { protocol: TCP, port: 53 } + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - 169.254.169.254/32 + ports: + - { protocol: TCP, port: 443 } diff --git a/cluster/applications/obsidian-vault/obsidian-sync.yaml b/cluster/applications/obsidian-vault/obsidian-sync.yaml new file mode 100644 index 0000000..d1fb6b9 --- /dev/null +++ b/cluster/applications/obsidian-vault/obsidian-sync.yaml @@ -0,0 +1,18 @@ +# DO NOT COMMIT real values. Create out-of-band in namespace "obsidian": +# +# ob login # once, on a trusted machine +# kubectl create namespace obsidian # if Argo hasn't created it yet +# kubectl -n obsidian create secret generic obsidian-sync \ +# --from-file=auth_token= \ +# --from-literal=vault_name='' \ +# --from-literal=e2ee_password='' +apiVersion: v1 +kind: Secret +metadata: + name: obsidian-sync + namespace: obsidian +type: Opaque +stringData: + auth_token: "REPLACE" + vault_name: "REPLACE" + # e2ee_password: "REPLACE" diff --git a/cluster/applications/obsidian-vault/pvcs.yaml b/cluster/applications/obsidian-vault/pvcs.yaml new file mode 100644 index 0000000..96eec79 --- /dev/null +++ b/cluster/applications/obsidian-vault/pvcs.yaml @@ -0,0 +1,29 @@ +# Vault copy. One writer (the sync pod), so RWO is enough for now. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: obsidian-vault + annotations: + argocd.argoproj.io/sync-wave: "0" + argocd.argoproj.io/sync-options: Prune=false +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: px-fa-direct-access + resources: + requests: + storage: 10Gi # size to your vault + attachments, with headroom +--- +# Client config + state.db, so restarts don't trigger a full re-index. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: obsidian-sync-state + annotations: + argocd.argoproj.io/sync-wave: "0" + argocd.argoproj.io/sync-options: Prune=false +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: px-fa-direct-access + resources: + requests: + storage: 1Gi