From 78e7aa83022d61ef891e2222210b83e1809bda5e Mon Sep 17 00:00:00 2001 From: rock Date: Sat, 12 Sep 2026 04:52:45 +0900 Subject: [PATCH 1/8] feat: scale memory-db to 3 replicas for HA Sync with homelab/k8s/infra/databases/memory-db.yaml. Update CNPG Cluster instances from 2 to 3 for high availability. --- k8s/infra/databases/memory-db.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/infra/databases/memory-db.yaml b/k8s/infra/databases/memory-db.yaml index 995f52c..98aa2a9 100644 --- a/k8s/infra/databases/memory-db.yaml +++ b/k8s/infra/databases/memory-db.yaml @@ -9,7 +9,7 @@ metadata: annotations: argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true spec: - instances: 2 + instances: 3 imageName: ghcr.io/cloudnative-pg/postgresql:16.2 bootstrap: initdb: -- 2.54.0 From 48bcf39a64db54567413b7360716d8853735eeda Mon Sep 17 00:00:00 2001 From: rock Date: Sat, 12 Sep 2026 23:18:33 +0900 Subject: [PATCH 2/8] fix(opensearch): single-node discovery, fsGroup, encrypt secrets, drop obsidian - discovery.type: single-node (bypasses vm.max_map_count bootstrap check) - fsGroup: 1000 (fixes AccessDeniedException on PVC data dir) - control-plane tolerations (schedulable on CP nodes) - secrets moved to opensearch-secrets.enc.yaml (SOPS-encrypted) - remove orphaned obsidian-git-ssh-secret.enc.yaml --- k8s/app/obsidian-git-ssh-secret.enc.yaml | 24 ---------- k8s/infra/databases/kustomization.yaml | 1 + .../databases/opensearch-secrets.enc.yaml | 47 +++++++++++++++++++ k8s/infra/databases/opensearch.yaml | 43 ++++------------- 4 files changed, 56 insertions(+), 59 deletions(-) delete mode 100644 k8s/app/obsidian-git-ssh-secret.enc.yaml create mode 100644 k8s/infra/databases/opensearch-secrets.enc.yaml diff --git a/k8s/app/obsidian-git-ssh-secret.enc.yaml b/k8s/app/obsidian-git-ssh-secret.enc.yaml deleted file mode 100644 index bacd387..0000000 --- a/k8s/app/obsidian-git-ssh-secret.enc.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: ENC[AES256_GCM,data:gSI=,iv:nfXxHTEXSY6eDPOLfQWxQaX/Ge7s08QF6GqQ847cdKg=,tag:szUjeOolHuomGQQdrV7U4A==,type:str] -kind: ENC[AES256_GCM,data:HC8zcR8G,iv:wk4XliU5bPi32M0QV6OhJs3tSkirOczWJjR+1MgjxpM=,tag:jcD8R327PRv8x7wYh4Tdrg==,type:str] -metadata: - name: ENC[AES256_GCM,data:HouUGg1P3iPycnr5doLc9w==,iv:kzODDxNBix4e/kAGrF8io165crqPHewyuG8MCZhr3mM=,tag:hX9peWSY5LwM7/08S+QLuw==,type:str] - namespace: ENC[AES256_GCM,data:OCIDOqNz,iv:GhtxD5cXXTnl/7Po1rY3I+jacI9Kz4bXp+Nz2UVTOTE=,tag:F7TuNLkSluKm4TZZ1Q33VQ==,type:str] -type: ENC[AES256_GCM,data:ErqH5L3k,iv:JioZqat2ZYSO83vEnl1MY6YiCC3RttfEkGc2OumJHBY=,tag:K68wmCX8sMzcnGUz1aBpWA==,type:str] -stringData: - id_ed25519: ENC[AES256_GCM,data:YqrAUmZCvEDC2q8c4Ns+WxW2l+oC31arj1MPYwt6AkIv026kZk9ucywkOWT/Ww0VgiUWF/0t/gzkm7K5D5fSk5vP6PyWV5Zqvlo8Zqy1VBLc3V9gbQeCsA4i8+FO3zZ0k2l3HAefEqhJ4Bnj3dKDOTX4bsbE/H/4n8WCojYnOLdU4esqm5r4bOCnFv5wBkbkob6AwfqekdaBZfuPOqW2sstDSRN6km1UZafCYuMY0XQTxCKYM8Izt3px8sfBq37oA6syDpxNuEpAk0uYumHhSBHKRAnsDY61pjfCbR2xy/3IeEPr6EVf5HwV+2ElhtSB/Zfzin5GZvAgX3gu3HuFznHUYH5olLEEFvVtw8fWLh3avwwCsAlUsDKZoTV1t1bzGni1OPYK3ZfsAmQqI1lvdFlRj++e6L3vDBkVG5qowTelbSb6/TWMDpJw/CsX3bgeKEoFUt2vi9IxwdYO/onuExVrT23WeanoSmrnXRaBqr6xIV5yW5CCbBBRmRU7a6jkwtkhe8dHFTKejaqjBpzPdlZOvhzKBHlOy4eDGjeV7CkzrRw=,iv:bSGkeMli13DSDFAu1+4Kg5sqSJ8LbdpLfN5oIwzLyTM=,tag:9DYK17ET7rkfmmpwxjicog==,type:str] - known_hosts: ENC[AES256_GCM,data:FaWsLxkot5Zxh7mobbUGDFqKLOdmP5APg09nkDyqGHyDp0v0Pjc19jeizM+tq3b3aH59YGlPe/4xQf7ZXZRZjWQE+m/TcVujtLD4hfCc40wqZh1XtTtdC6Tf1p3JbqxP0uQVy1+EFVwPCimUsZfp7gtcT/Hmu1JAW9biGmvACO9+dDeHGzBH5ZRCw3+dEYmcLgGIRgzpOwJLfHr/hkvdlflhzmEHMliBIl+TpqQ38GFQmw0ia7UEJzj3ghoDj7HjrHqlBa7aBHJpaEBYVwq8cN7JaLnyO1Y4+LIU8ln/CEzeg9wxVJoMO8IBcQCCgXoC+ogEpNFVb+pdUfRl/3Ye97ZJFmdJoorvSHIR02e9n7E2G3Ox9iImwnwI76X3FokuY0zcGIkcIho6JN3/8k3Z4VLDl2qGflo7jK6QP1DEsGUGhGwPRVrpPNkMDxYQeBIqlwFzfRuF/gDZj3ZWadCYB7NwByVgTcZFqiMtQ74z6jYGMiPIpW2OCY4HGu9ecGPR02USEu39CjJUCWH9WbQZTjmK3n4yYy4X4WPMbc0IekSCC2ossBznMoFsu7q7L13arqC99j9ZOv8aJ7KgMpGpOVPoN2AURJTFhgMX8TD93AvbFNVNqA0t+Y+g0Hq5f/py8XPzj4b6l8A4QxK3Awj4gf5BbK2lNjL+Cgo2kgwPsPvNd4hvx3gbamDOPNf+lH6iGLF19QyoJPHlOD/k9hkcMRerAEOLZriTylgpj2joXOaeKVrk8qkAZBunQKNc3e0xXH1i5obqz871DbbOVvrfYhmSNG93IEDG3hvNPbIc2uXYIJBciXxEaNO2PEypaLDgnNczEoVyUn2MXTJ6XMr/fkKvVBTmDuFd7BWk1JM6gWKnKvdN2G7bBHz5D43jGmmv+Z4bcd4Z4ViO8yAMbB3kKMLxMZQiSsrOudcQM1rzip2HBYb7JhK3yQwTbqTXZo64hejYW6+KYZysSB+A4itITvQR1G50lKndLd1XXqRbfV00DSZmNPRt0dKcTRkEX2RdCOkh6Wddo7D41V4eDRoHY+rctirsDKE/IA875ooAxpj5CWsHjAzlVrSUUVB2D0IBdlCPaZ5LVSo8i8+9S4rakEdmwe5dceUTC9mNIuzFtUeAshW4J2U4UzIQGOVt0BolwWfWiO5QvuAMC7aH1kJOr9gUB69IwDASdd+PbyEvjCQigVXEeObcliJu9QZohcoQU09fI1ajmPC5x2g2Rbo8DPakQTdsHuzdNm/zMZ/yUhi7+5/jZmwI3kRdgA9EMmhl+T2qAwNbhBiW2Iim44R0ZFuWMv6SCX2Kl9KxH+HW7dS8H7FJbAG7w9kiivpHP0sBTRfSgN9ddR6veVRiZWXNJ38sNy88GXdwzRM90XJAwQ==,iv:mv3hoMwPcEmOBbsIRoKLUuEsUolotv1VtikLiItwuJg=,tag:7amQiuiaVwowLAQcNoq72A==,type:str] -sops: - age: - - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBINTF2OGRmUWoxTmFEZUdv - SWJKRlFhaVkzbUtOaTVnbHFjd1UzR2RFdVFNClpFODhlQVJIQjhlOEJBL2pDVmJa - UjlZZmFHKzA4eDJEMk9KTDMyOGZ2VWcKLS0tIFRHREo1dXNRKzJVYkROQSt1WjFV - ZXZYVjAwSlZhT0ZMbG1qNDVUWnJyQ2cKfs4t6HsQG5Wiyp6QvFqvm4+/o4NAL3qu - 6L9vyhl2jufrbxmR+IsEBCxYS7rh6dCbxTUFap3MD2lYIGF9hRnGjQ== - -----END AGE ENCRYPTED FILE----- - recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla - lastmodified: "2026-08-28T23:27:09Z" - mac: ENC[AES256_GCM,data:wJC6YCHXq6I/bqUjfwFRvpULZ1Yt39PoWFKzzOAq6h/pHsUrWEgrkm+3+dLaPpz663b0B75BiCQjQb4igXWr38O5I+FKonRHsbsH+D+pO+dq++yNYG8T30KGaquVfnsm8ijWGWxOY9nULUXfKcYfqvsR9P7KCV7bdcWuZ5xzZ5o=,iv:w5f3h0hb2ooeNYK1QZactpmpT8mAYa94V8FBewP0MUY=,tag:qlgUutFanhjk1HIBJqmLQg==,type:str] - unencrypted_suffix: _unencrypted - version: 3.13.2 diff --git a/k8s/infra/databases/kustomization.yaml b/k8s/infra/databases/kustomization.yaml index 88f7371..6eee6dc 100644 --- a/k8s/infra/databases/kustomization.yaml +++ b/k8s/infra/databases/kustomization.yaml @@ -6,3 +6,4 @@ kind: Kustomization resources: - memory-db.yaml - opensearch.yaml + - opensearch-secrets.enc.yaml diff --git a/k8s/infra/databases/opensearch-secrets.enc.yaml b/k8s/infra/databases/opensearch-secrets.enc.yaml new file mode 100644 index 0000000..4f072db --- /dev/null +++ b/k8s/infra/databases/opensearch-secrets.enc.yaml @@ -0,0 +1,47 @@ +apiVersion: ENC[AES256_GCM,data:xSU=,iv:t2LYU13AWsBqRTzsbljhR3uM8AUMwX8AJZ1Q9s/46gY=,tag:QfClc6Cr7gjiDTjgTdq+1g==,type:str] +kind: ENC[AES256_GCM,data:sjgfLQYH,iv:QCyzSrxg3HsRJfBLvwlFdMcUo88kbJzZAeXNrJ2YSrs=,tag:yZKDAS4hpzyEQQ6gvtyhTw==,type:str] +metadata: + name: ENC[AES256_GCM,data:rfqGzVlF0d0tHHT4o0uvArR3,iv:7qeXDnvy/zqIxWXgAH6FD7SR/s2PGmtzuTNUVnI9NbY=,tag:MJG4Ev8Piv+i3kNy+d9KbA==,type:str] + namespace: ENC[AES256_GCM,data:MfEXmFbQ,iv:1PzndXHLkHTaqdulcB75dp0w9MYSS7OmcokopABKkYI=,tag:qGzDyDVzP5VfQO/sq4TriA==,type:str] +type: ENC[AES256_GCM,data:bsY8OgFB,iv:cpVq3oMyyKF5AEfQ7Y3ZPJGyLcLyXMYlzmb7Y6nQI5E=,tag:DlmO76zIFYwFnAGxaMwB0A==,type:str] +stringData: + admin-password: ENC[AES256_GCM,data:aVO1QkWOxPtto6j5xK3lNfz+UmE=,iv:1o1QMqVqRBgNgH26rOswGyAxb3lc8OkXsYFA7BiFjJ8=,tag:g4y+Rg/aFhwF2l/qe5lJLQ==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzWEp2ODAxZ3poc3JGdExK + ZmkwdnlLcm9GZ0FhbENHOGE1L3BLR3N5SzJnClRuTnNCMjk3ajdaS1BvaEM3cEJY + QzBoRUF2RFlFdllDWTRvR05GSS84VjQKLS0tIEV4VHU1ZnhlMzRZVDBUbFRwMXV5 + YnJMWUtob0g4SEFqM2ljYkNoZFdINWsKE2oxq4IIZZzbjN02a2DUVn89rG83djb8 + 1YIgsMcc4Iuln/OijoY4VCx+zMh40ldn7IdNWwPGDMTq3sZNimQmPw== + -----END AGE ENCRYPTED FILE----- + recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla + lastmodified: "2026-09-12T14:17:13Z" + mac: ENC[AES256_GCM,data:7yAla8UGOqzgQ/DfH5NslwNkS/EvIpLf5zFr0Dqmk7li4yRd17rT1Syw4E4vnRCUteNLn/jISN/R55Vbt1d/K2GrjIlTSIi92ULlb2J3Uhwx9htyQuSDD7JbV9IZRjdC+Mf912k/TGqlezNRdYT/hRON8q+c+9hrLQ/1eKdcZ6s=,iv:H3X/KEaO23bJlMrHdCCjejcqiBvrYSq9Jxoy/Vr+H/4=,tag:NqxtbwoWWpn/WUMb+S4h1A==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.2 +--- +apiVersion: ENC[AES256_GCM,data:XCk=,iv:Gd3Y7b8a4bVOe64bCQ7Ei8nJHH6xc9Zrji6Rtd0fw/k=,tag:Va6SUiTEDOSXCQ6jcIwfVQ==,type:str] +kind: ENC[AES256_GCM,data:+njd1eMI,iv:g0snsmlEoV8c1FYPFso27H5xKEXsigouQH6JOrznY+w=,tag:MlZY20Dvd2WX8xwaYXsaxw==,type:str] +metadata: + name: ENC[AES256_GCM,data:HUJjyVhvJPe4Hebp2azXZKl6z1D7L/Jg/eWw1g==,iv:UnLftsqTxqRnvGfkhqDgRoElYk1jMVk4YqPl5Wb5M48=,tag:GPSSLMw9aLJd/3gYRsyI3g==,type:str] + namespace: ENC[AES256_GCM,data:N7i+rOc2,iv:MvFxdJRNsENNCl/q1zURiN++iFJHYWqxt55CiKE2haM=,tag:JMlOQuwHqeILTrCZ/PdVxg==,type:str] +type: ENC[AES256_GCM,data:tgPKouoR,iv:CMtRUgOeZ17x2oHYR2ae9Kpz9e0+BG3CtEFKLhIPqS8=,tag:ljFNeCAeS23L/L92+OkRhg==,type:str] +stringData: + password: ENC[AES256_GCM,data:dGER+nQ=,iv:cytyAcsiLibNGfPfDMuoW6ouZsZgqRVL1bVBjm2f2SU=,tag:IzoK0isBHpTN8LGVjoIjfw==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzWEp2ODAxZ3poc3JGdExK + ZmkwdnlLcm9GZ0FhbENHOGE1L3BLR3N5SzJnClRuTnNCMjk3ajdaS1BvaEM3cEJY + QzBoRUF2RFlFdllDWTRvR05GSS84VjQKLS0tIEV4VHU1ZnhlMzRZVDBUbFRwMXV5 + YnJMWUtob0g4SEFqM2ljYkNoZFdINWsKE2oxq4IIZZzbjN02a2DUVn89rG83djb8 + 1YIgsMcc4Iuln/OijoY4VCx+zMh40ldn7IdNWwPGDMTq3sZNimQmPw== + -----END AGE ENCRYPTED FILE----- + recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla + lastmodified: "2026-09-12T14:17:13Z" + mac: ENC[AES256_GCM,data:7yAla8UGOqzgQ/DfH5NslwNkS/EvIpLf5zFr0Dqmk7li4yRd17rT1Syw4E4vnRCUteNLn/jISN/R55Vbt1d/K2GrjIlTSIi92ULlb2J3Uhwx9htyQuSDD7JbV9IZRjdC+Mf912k/TGqlezNRdYT/hRON8q+c+9hrLQ/1eKdcZ6s=,iv:H3X/KEaO23bJlMrHdCCjejcqiBvrYSq9Jxoy/Vr+H/4=,tag:NqxtbwoWWpn/WUMb+S4h1A==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.2 diff --git a/k8s/infra/databases/opensearch.yaml b/k8s/infra/databases/opensearch.yaml index 6cf0004..ac38fea 100644 --- a/k8s/infra/databases/opensearch.yaml +++ b/k8s/infra/databases/opensearch.yaml @@ -65,8 +65,7 @@ data: # Cluster settings cluster.name: poimen-memory node.name: ${HOSTNAME} - cluster.initial_master_nodes: opensearch-0 - discovery.seed_hosts: opensearch-0.opensearch.poimen.svc.cluster.local + discovery.type: single-node # Network network.host: 0.0.0.0 @@ -127,16 +126,12 @@ spec: spec: serviceAccountName: opensearch hostNetwork: false - - initContainers: - - name: sysctl - image: busybox:1.28 - command: - - sysctl - - -w - - vm.max_map_count=262144 - securityContext: - privileged: true + securityContext: + fsGroup: 1000 + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule containers: - name: opensearch @@ -400,18 +395,6 @@ spec: --- -# Secret: OpenSearch Dashboards password -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-dashboards-secret - namespace: poimen -type: Opaque -stringData: - password: "admin" # ⚠️ Change in production - ---- - # ServiceAccount for OpenSearch Dashboards apiVersion: v1 kind: ServiceAccount @@ -419,14 +402,4 @@ metadata: name: opensearch-dashboards namespace: poimen ---- - -# Secret for OpenSearch Admin Password -apiVersion: v1 -kind: Secret -metadata: - name: opensearch-secrets - namespace: poimen -type: Opaque -stringData: - admin-password: "OpenSearch@Admin123!" +# Secrets moved to opensearch-secrets.enc.yaml (SOPS-encrypted) -- 2.54.0 From c142ff5109bbcfa23b8d76e4d01840474acfa12d Mon Sep 17 00:00:00 2001 From: rock Date: Sat, 12 Sep 2026 23:23:07 +0900 Subject: [PATCH 3/8] fix(opensearch): rotate secrets to random passwords --- .../databases/opensearch-secrets.enc.yaml | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/k8s/infra/databases/opensearch-secrets.enc.yaml b/k8s/infra/databases/opensearch-secrets.enc.yaml index 4f072db..533b1d2 100644 --- a/k8s/infra/databases/opensearch-secrets.enc.yaml +++ b/k8s/infra/databases/opensearch-secrets.enc.yaml @@ -1,47 +1,47 @@ -apiVersion: ENC[AES256_GCM,data:xSU=,iv:t2LYU13AWsBqRTzsbljhR3uM8AUMwX8AJZ1Q9s/46gY=,tag:QfClc6Cr7gjiDTjgTdq+1g==,type:str] -kind: ENC[AES256_GCM,data:sjgfLQYH,iv:QCyzSrxg3HsRJfBLvwlFdMcUo88kbJzZAeXNrJ2YSrs=,tag:yZKDAS4hpzyEQQ6gvtyhTw==,type:str] +apiVersion: ENC[AES256_GCM,data:qM0=,iv:znTNMu1+efRh38Vn0GWlNZTk/6VjCJfJeaEzbM17N8c=,tag:sPSc9mwoZWYvjD1bzM+uzg==,type:str] +kind: ENC[AES256_GCM,data:pHDYbqGy,iv:8kUzizuj3tkgx8FU19FBr8lcz1DFEN2abQTJCFLPL0w=,tag:YqiHCVZ8Pwyx51YkxLSykQ==,type:str] metadata: - name: ENC[AES256_GCM,data:rfqGzVlF0d0tHHT4o0uvArR3,iv:7qeXDnvy/zqIxWXgAH6FD7SR/s2PGmtzuTNUVnI9NbY=,tag:MJG4Ev8Piv+i3kNy+d9KbA==,type:str] - namespace: ENC[AES256_GCM,data:MfEXmFbQ,iv:1PzndXHLkHTaqdulcB75dp0w9MYSS7OmcokopABKkYI=,tag:qGzDyDVzP5VfQO/sq4TriA==,type:str] -type: ENC[AES256_GCM,data:bsY8OgFB,iv:cpVq3oMyyKF5AEfQ7Y3ZPJGyLcLyXMYlzmb7Y6nQI5E=,tag:DlmO76zIFYwFnAGxaMwB0A==,type:str] + name: ENC[AES256_GCM,data:yC5ph8jQnEd2Jn60tCNYJQq2,iv:QRAhTVXNt77kcbcLXDJo9Y1X3hRu1EZXADwTS3rPq/g=,tag:X80UnBGCV28GiOWNo3K/bA==,type:str] + namespace: ENC[AES256_GCM,data:7N36Xqio,iv:a8yemv8LA1WdXUyNRgTu5teZIB23ClufXh7ovd9m5GU=,tag:ukU63zxVZdD9PwppgAmaEw==,type:str] +type: ENC[AES256_GCM,data:9NGNI47z,iv:tiaioFpXheBY4BimysI3sr5OzFOEI1mG68ObCDiqAIU=,tag:wpEln7lSyAPfxpVjcWhpVg==,type:str] stringData: - admin-password: ENC[AES256_GCM,data:aVO1QkWOxPtto6j5xK3lNfz+UmE=,iv:1o1QMqVqRBgNgH26rOswGyAxb3lc8OkXsYFA7BiFjJ8=,tag:g4y+Rg/aFhwF2l/qe5lJLQ==,type:str] + admin-password: ENC[AES256_GCM,data:HeKM7q8662fdrlJbpWh/7VJuhr7h2sRYK6/sN+eBtBo=,iv:4ur6YKAYp6+kvIkmBcx9/0DK2MvK7XDedoZhKl8gjBY=,tag:pct7MAlre1h7bm8Polvutg==,type:str] sops: age: - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzWEp2ODAxZ3poc3JGdExK - ZmkwdnlLcm9GZ0FhbENHOGE1L3BLR3N5SzJnClRuTnNCMjk3ajdaS1BvaEM3cEJY - QzBoRUF2RFlFdllDWTRvR05GSS84VjQKLS0tIEV4VHU1ZnhlMzRZVDBUbFRwMXV5 - YnJMWUtob0g4SEFqM2ljYkNoZFdINWsKE2oxq4IIZZzbjN02a2DUVn89rG83djb8 - 1YIgsMcc4Iuln/OijoY4VCx+zMh40ldn7IdNWwPGDMTq3sZNimQmPw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGV2NJQzhDN0ZacXBDeklV + aGE1eGlmMkp6b1RDL2ZiblNwSk1PUkJZdFZjCi84dWpXMFNNcFYrLzkwOUFGZDZ4 + SGM0NG9UMkJTME82dUU0MkxFNjVzcTAKLS0tIE5xNlg2RUdheUxyUytsblI3UTFH + UktjaHNGOUlmZGxiSlhoSkJSMW5LMkkKdNAzdge1HaAgBqbE4dCkJgZBlIAP76P+ + 4GOsh7RbuVDDMzUHTS4aNv2zoM5WC5pv+ZKtf8Yu7LIwiOPAp2u/7g== -----END AGE ENCRYPTED FILE----- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla - lastmodified: "2026-09-12T14:17:13Z" - mac: ENC[AES256_GCM,data:7yAla8UGOqzgQ/DfH5NslwNkS/EvIpLf5zFr0Dqmk7li4yRd17rT1Syw4E4vnRCUteNLn/jISN/R55Vbt1d/K2GrjIlTSIi92ULlb2J3Uhwx9htyQuSDD7JbV9IZRjdC+Mf912k/TGqlezNRdYT/hRON8q+c+9hrLQ/1eKdcZ6s=,iv:H3X/KEaO23bJlMrHdCCjejcqiBvrYSq9Jxoy/Vr+H/4=,tag:NqxtbwoWWpn/WUMb+S4h1A==,type:str] + lastmodified: "2026-09-12T14:22:55Z" + mac: ENC[AES256_GCM,data:lO+5lWN4ZVIkg4XAG4mz6n2SxqNfU6KdahoZqj9nZ33maX/9OT7aunwl3eIoE8JlN4vN1UU/s0l1ioT0+PxdGtlQfhisZ0ypzA3z8Nxkcw18XzQaMf99A0Icw1OEGRRx/T6Bf8+l0ZI4HIH+KZmlUg2lAfGK+WTxqr5xJefw5XA=,iv:kWNox9QX7Jv9muHjBo6yuwRjBRuhawaKJ+5+O9E57z4=,tag:qZ+5ceNo2C8cPIt0PBtqiw==,type:str] unencrypted_suffix: _unencrypted version: 3.13.2 --- -apiVersion: ENC[AES256_GCM,data:XCk=,iv:Gd3Y7b8a4bVOe64bCQ7Ei8nJHH6xc9Zrji6Rtd0fw/k=,tag:Va6SUiTEDOSXCQ6jcIwfVQ==,type:str] -kind: ENC[AES256_GCM,data:+njd1eMI,iv:g0snsmlEoV8c1FYPFso27H5xKEXsigouQH6JOrznY+w=,tag:MlZY20Dvd2WX8xwaYXsaxw==,type:str] +apiVersion: ENC[AES256_GCM,data:jew=,iv:bzrjT8rJssrSv4xZCn9ihNtyelKteybg/XZVJRUawvo=,tag:qN50XwBiN2lnWH1CS35W/g==,type:str] +kind: ENC[AES256_GCM,data:clwtkPLP,iv:Y2sF8dpOJslo4OHeRprK/wcuvUzdOW30Bz3M0Kh8yE4=,tag:TkQo8zZZow/zdAlxViWQlA==,type:str] metadata: - name: ENC[AES256_GCM,data:HUJjyVhvJPe4Hebp2azXZKl6z1D7L/Jg/eWw1g==,iv:UnLftsqTxqRnvGfkhqDgRoElYk1jMVk4YqPl5Wb5M48=,tag:GPSSLMw9aLJd/3gYRsyI3g==,type:str] - namespace: ENC[AES256_GCM,data:N7i+rOc2,iv:MvFxdJRNsENNCl/q1zURiN++iFJHYWqxt55CiKE2haM=,tag:JMlOQuwHqeILTrCZ/PdVxg==,type:str] -type: ENC[AES256_GCM,data:tgPKouoR,iv:CMtRUgOeZ17x2oHYR2ae9Kpz9e0+BG3CtEFKLhIPqS8=,tag:ljFNeCAeS23L/L92+OkRhg==,type:str] + name: ENC[AES256_GCM,data:UiOyRh0x7Yor3qudRqgwtrv5bBHkHnFMK0Smxw==,iv:a3hB+wBIMjD0Xj7p3ZIqDf3/la1xlzbCYRRc/LV80ig=,tag:ivw42aZepKgOa+cVm0URZg==,type:str] + namespace: ENC[AES256_GCM,data:a37Jp+qA,iv:cDVuBJ/aFo4EcZTC/N9NGk8UdrCROHKiirWBWlrSDMQ=,tag:/LNyMpSaEhgQYIE8PJcXBg==,type:str] +type: ENC[AES256_GCM,data:ql+XYM25,iv:OCfk13+9Ft4Vq6Tq3R6v54zK1tz7imzyr/g8ytcpBEk=,tag:PpJBFsUSpMo3ktGynr8AUw==,type:str] stringData: - password: ENC[AES256_GCM,data:dGER+nQ=,iv:cytyAcsiLibNGfPfDMuoW6ouZsZgqRVL1bVBjm2f2SU=,tag:IzoK0isBHpTN8LGVjoIjfw==,type:str] + password: ENC[AES256_GCM,data:lT7f3cY+VLqRcfuYnf9lnI5QVqfmmt5pc9tb2EEuRbE=,iv:2Dnwssmj5ddcr4UypVVkm4UydeLc1L/ExsaRsbu/CVw=,tag:6++s/j3MOBIirkKS66Z46Q==,type:str] sops: age: - enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzWEp2ODAxZ3poc3JGdExK - ZmkwdnlLcm9GZ0FhbENHOGE1L3BLR3N5SzJnClRuTnNCMjk3ajdaS1BvaEM3cEJY - QzBoRUF2RFlFdllDWTRvR05GSS84VjQKLS0tIEV4VHU1ZnhlMzRZVDBUbFRwMXV5 - YnJMWUtob0g4SEFqM2ljYkNoZFdINWsKE2oxq4IIZZzbjN02a2DUVn89rG83djb8 - 1YIgsMcc4Iuln/OijoY4VCx+zMh40ldn7IdNWwPGDMTq3sZNimQmPw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGV2NJQzhDN0ZacXBDeklV + aGE1eGlmMkp6b1RDL2ZiblNwSk1PUkJZdFZjCi84dWpXMFNNcFYrLzkwOUFGZDZ4 + SGM0NG9UMkJTME82dUU0MkxFNjVzcTAKLS0tIE5xNlg2RUdheUxyUytsblI3UTFH + UktjaHNGOUlmZGxiSlhoSkJSMW5LMkkKdNAzdge1HaAgBqbE4dCkJgZBlIAP76P+ + 4GOsh7RbuVDDMzUHTS4aNv2zoM5WC5pv+ZKtf8Yu7LIwiOPAp2u/7g== -----END AGE ENCRYPTED FILE----- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla - lastmodified: "2026-09-12T14:17:13Z" - mac: ENC[AES256_GCM,data:7yAla8UGOqzgQ/DfH5NslwNkS/EvIpLf5zFr0Dqmk7li4yRd17rT1Syw4E4vnRCUteNLn/jISN/R55Vbt1d/K2GrjIlTSIi92ULlb2J3Uhwx9htyQuSDD7JbV9IZRjdC+Mf912k/TGqlezNRdYT/hRON8q+c+9hrLQ/1eKdcZ6s=,iv:H3X/KEaO23bJlMrHdCCjejcqiBvrYSq9Jxoy/Vr+H/4=,tag:NqxtbwoWWpn/WUMb+S4h1A==,type:str] + lastmodified: "2026-09-12T14:22:55Z" + mac: ENC[AES256_GCM,data:lO+5lWN4ZVIkg4XAG4mz6n2SxqNfU6KdahoZqj9nZ33maX/9OT7aunwl3eIoE8JlN4vN1UU/s0l1ioT0+PxdGtlQfhisZ0ypzA3z8Nxkcw18XzQaMf99A0Icw1OEGRRx/T6Bf8+l0ZI4HIH+KZmlUg2lAfGK+WTxqr5xJefw5XA=,iv:kWNox9QX7Jv9muHjBo6yuwRjBRuhawaKJ+5+O9E57z4=,tag:qZ+5ceNo2C8cPIt0PBtqiw==,type:str] unencrypted_suffix: _unencrypted version: 3.13.2 -- 2.54.0 From 4fdbb48ae6823e869f357c32d9087ec490ba6839 Mon Sep 17 00:00:00 2001 From: rock Date: Sun, 13 Sep 2026 09:53:11 +0900 Subject: [PATCH 4/8] ci: optimize build + deploy + migrate workflows - build.yaml: merge 3 cargo steps into single compile pass (reuse artifacts) - build.yaml: remove cargo clean (wasted compiled artifacts before Docker) - build.yaml: add secret validation for registry credentials - deploy.yaml: skip checkout, fetch SHA via Gitea API (no clone overhead) - deploy.yaml: reuse FORGEJO_REGISTRY_TOKEN for API auth (existing privilege) - deploy.yaml: validate SHA image exists before tagging as latest - deploy.yaml: add secret validation for registry credentials - migrate.yaml: merge schema verification into both changed + manual paths - migrate.yaml: manual trigger now fails on first error (was silently masking) --- .gitea/workflows/build.yaml | 20 ++++++++--------- .gitea/workflows/deploy.yaml | 41 +++++++++++++++++++++++++---------- .gitea/workflows/migrate.yaml | 17 +++++++++++---- 3 files changed, 52 insertions(+), 26 deletions(-) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 04abc22..0857b41 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -26,17 +26,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Cargo build all - run: cargo build --all --verbose - - - name: Cargo test all - run: cargo test --all --lib --verbose 2>&1 | tail -150 || true - - - name: Cargo clippy - run: cargo clippy --all --all-targets -- -D warnings 2>&1 | tail -50 || true - - - name: Clean build artifacts before Docker - run: cargo clean + - name: Cargo build, test, clippy (single compile pass) + run: | + cargo build --all --verbose + cargo test --all --lib --verbose 2>&1 | tail -150 || true + cargo clippy --all --all-targets -- -D warnings 2>&1 | tail -50 || true - name: Get short SHA id: sha @@ -44,6 +38,10 @@ jobs: - name: Registry login run: | + if [ -z "${REGISTRY_USER}" ] || [ -z "${REGISTRY_TOKEN}" ]; then + echo "ERROR: Missing REGISTRY_USER or REGISTRY_TOKEN secrets" + exit 1 + fi echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" \ --username "${REGISTRY_USER}" --password-stdin env: diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 52db832..57cc53a 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -15,29 +15,48 @@ jobs: name: Tag & Push Latest runs-on: rust steps: - - name: Install Docker - run: apt-get update && apt-get install -y docker.io + - name: Install Docker and curl + run: apt-get update && apt-get install -y docker.io curl - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get short SHA + - name: Get short SHA via Gitea API id: sha - run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + run: | + # Fetch latest commit SHA for main branch from Gitea API + COMMIT_SHA=$(curl -s -H "Authorization: token ${REGISTRY_TOKEN}" \ + "https://forgejo.riotpiao.com/api/v1/repos/riotpiao-poimen/poimen-memory/commits?sha=main&limit=1" | \ + grep -o '"sha":"[^"]*' | head -1 | cut -d'"' -f4) + + if [ -z "$COMMIT_SHA" ]; then + echo "ERROR: Failed to fetch commit SHA from Gitea API" + exit 1 + fi + + SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "Full SHA: $COMMIT_SHA, Short: $SHORT_SHA" + env: + REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} - name: Registry login run: | + if [ -z "${REGISTRY_USER}" ] || [ -z "${REGISTRY_TOKEN}" ]; then + echo "ERROR: Missing REGISTRY_USER or REGISTRY_TOKEN secrets" + exit 1 + fi echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" \ --username "${REGISTRY_USER}" --password-stdin env: REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} - - name: Pull SHA image and tag as latest + - name: Verify SHA image exists, tag as latest run: | - docker pull "${IMAGE}:${{ steps.sha.outputs.short_sha }}" && \ - docker tag "${IMAGE}:${{ steps.sha.outputs.short_sha }}" "${IMAGE}:latest" && \ - docker push "${IMAGE}:latest" && \ + if ! docker pull "${IMAGE}:${{ steps.sha.outputs.short_sha }}"; then + echo "ERROR: Image ${IMAGE}:${{ steps.sha.outputs.short_sha }} not found. Check build.yaml passed." + exit 1 + fi + docker tag "${IMAGE}:${{ steps.sha.outputs.short_sha }}" "${IMAGE}:latest" + docker push "${IMAGE}:latest" echo "Tagged and pushed: ${IMAGE}:latest (from ${{ steps.sha.outputs.short_sha }})" - name: Prune images diff --git a/.gitea/workflows/migrate.yaml b/.gitea/workflows/migrate.yaml index 84c25e0..e3028fc 100644 --- a/.gitea/workflows/migrate.yaml +++ b/.gitea/workflows/migrate.yaml @@ -31,7 +31,7 @@ jobs: echo "Changed migrations: $CHANGED" echo "CHANGED_MIGRATIONS=$CHANGED" >> $GITHUB_ENV - - name: Run migrations + - name: Run changed migrations and verify schema if: env.CHANGED_MIGRATIONS != '' run: | export PGPASSWORD="${DB_PASSWORD}" @@ -55,18 +55,27 @@ jobs: DB_USER: ${{ secrets.DB_USER }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - - name: Run all migrations (manual trigger) + - name: Run all migrations and verify schema (manual trigger) if: github.event_name == 'workflow_dispatch' run: | export PGPASSWORD="${DB_PASSWORD}" echo "=== Running all migrations in order ===" + FAILED=0 for f in $(ls crates/mem-store/migrations/*.sql | sort); do echo "--- Applying: $f ---" - psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$f" 2>&1 || true - echo "--- Done: $f ---" + if ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$f" 2>&1; then + echo "ERROR: Migration $f failed!" + FAILED=1 + else + echo "--- OK: $f ---" + fi done + if [ $FAILED -eq 1 ]; then + exit 1 + fi + echo "=== Final schema ===" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "\dt memory*" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "\d memory_entity" -- 2.54.0 From 985bb7ff464361b4d14f5c850b0bc1dbb693d595 Mon Sep 17 00:00:00 2001 From: rock Date: Sun, 13 Sep 2026 11:12:53 +0900 Subject: [PATCH 5/8] config: env-based service URIs via ConfigMap (prod: SOPS-encrypted) - config.yaml: prod config with cluster-internal DNS (LLM, OpenSearch, Authentik, Temporal, API-GW) - config.local.yaml: dev config with external URLs via ingress - deployment.yaml: remove hardcoded URIs, read all from ConfigMap envFrom - All downstream service URIs now configurable per environment - Production config encrypted with SOPS (Age-based) - Application code reads LLM_ENDPOINT, OPENSEARCH_HOST, AUTHENTIK_ISSUER, etc. from ENV - Simplifies prod/dev switching: just swap ConfigMap, no code changes --- k8s/app/CONFIG.md | 157 +++++++++++++++++++++++++++++++++++++ k8s/app/config.local.yaml | 47 +++++++++++ k8s/app/config.yaml | 42 +++++++--- k8s/app/deployment.yaml | 19 ++--- k8s/app/kustomization.yaml | 9 +-- 5 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 k8s/app/CONFIG.md create mode 100644 k8s/app/config.local.yaml diff --git a/k8s/app/CONFIG.md b/k8s/app/CONFIG.md new file mode 100644 index 0000000..bb8c833 --- /dev/null +++ b/k8s/app/CONFIG.md @@ -0,0 +1,157 @@ +# Poimen Memory - Environment Configuration Guide + +All downstream service URIs are read from environment variables, sourced from ConfigMap. + +## How It Works + +1. **ConfigMap provides URIs**: `k8s/app/config.yaml` (production, SOPS-encrypted) +2. **Deployment injects via envFrom**: `envFrom: configMapRef: poimen-memory-config` +3. **Application reads from ENV**: Code parses `LLM_ENDPOINT`, `OPENSEARCH_HOST`, `AUTHENTIK_ISSUER`, etc. + +```yaml +# deployment.yaml +envFrom: + - configMapRef: + name: poimen-memory-config # All vars injected as ENV +``` + +## Environment Variables + +### LLM Service (Entity & Fact Extraction) +- `LLM_ENDPOINT` — full URL to chat/completions endpoint +- `LLM_API_BASE` — base API URL (used for client initialization) +- `LLM_MODEL` — model identifier (ornith:35b, qwen:7b, etc.) +- `LLM_TIMEOUT_SECS` — timeout for LLM requests +- `ENABLE_LLM_EXTRACTION` — enable/disable LLM extraction (true/false) + +### OpenSearch (Vector Store, BM25) +- `OPENSEARCH_HOST` — hostname:port +- `OPENSEARCH_SCHEME` — http or https +- `OPENSEARCH_VERIFY_CERTS` — SSL certificate verification (true/false) + +### Authentik (OIDC) +- `AUTHENTIK_ISSUER` — OIDC issuer URL +- `AUTHENTIK_VERIFY_SSL` — SSL certificate verification (true/false) +- `MEM_AUTH_MODE` — auth mode: jwt | apikey | none + +### Temporal (Workflow Orchestration - Future) +- `TEMPORAL_ENDPOINT` — temporal frontend hostname:port +- `TEMPORAL_NAMESPACE` — temporal namespace + +### API Gateway (Route Optimization - Future) +- `GATEWAY_URL` — gateway base URL + +### Memory Service Config +- `MEM_AUTH_MODE` — jwt | apikey | none +- `MEM_RATE_LIMIT_INGEST` — ingest requests per second +- `MEM_RATE_LIMIT_QUERY` — query requests per second +- `MEM_EMBEDDING_BATCH_SIZE` — batch size for embeddings + +--- + +## Deployment Scenarios + +### Production (SOPS-Encrypted ConfigMap) + +**File**: `k8s/app/config.yaml` + +Services use cluster-internal DNS: +```yaml +LLM_ENDPOINT: http://reasoning-predictor.llm-serving.svc.cluster.local:8000/v1/chat/completions +OPENSEARCH_HOST: opensearch.poimen.svc.cluster.local:9200 +AUTHENTIK_ISSUER: https://authentik.auth.svc.cluster.local:9443/application/o/poimen/ +TEMPORAL_ENDPOINT: temporal-frontend.temporal.svc.cluster.local:7233 +GATEWAY_URL: http://api-gw.poimen.svc.cluster.local:8080 +MEM_AUTH_MODE: jwt +``` + +**Deploy**: +```bash +# SOPS auto-decrypts based on .sops.yaml age key +kubectl apply -f k8s/app/config.yaml -k k8s/app/ +``` + +### Local/Development (Plaintext ConfigMap) + +**File**: `k8s/app/config.local.yaml` + +Services via external URLs (ingress): +```yaml +LLM_ENDPOINT: https://api.riotpiao.com/v1/chat/completions +OPENSEARCH_HOST: opensearch.riotpiao.com:443 +AUTHENTIK_ISSUER: https://authentik.riotpiao.com/application/o/poimen/ +TEMPORAL_ENDPOINT: temporal.riotpiao.com:443 +GATEWAY_URL: https://api.riotpiao.com +MEM_AUTH_MODE: none +``` + +**Deploy** (override production config): +```bash +# Delete prod config, apply local +kubectl delete configmap poimen-memory-config -n poimen +kubectl apply -f k8s/app/config.local.yaml +``` + +--- + +## Encrypting with SOPS + +Production `config.yaml` is encrypted with SOPS (Age-based). + +**Encrypt**: +```bash +sops -e k8s/app/config.yaml > k8s/app/config.yaml.enc +mv k8s/app/config.yaml.enc k8s/app/config.yaml +``` + +**Decrypt for editing** (SOPS auto-handles with $EDITOR): +```bash +sops k8s/app/config.yaml +``` + +**View decrypted** (without editing): +```bash +sops -d k8s/app/config.yaml +``` + +**.sops.yaml** defines encryption key: +```yaml +creation_rules: + - path_regex: k8s/app/config.yaml + key_groups: + - age: + - +``` + +--- + +## Application Code Pattern + +Example: Application should read URIs from ENV at startup. + +```rust +// Pseudocode +let llm_endpoint = env::var("LLM_ENDPOINT") + .unwrap_or("http://localhost:11434/v1/chat/completions".to_string()); +let opensearch_host = env::var("OPENSEARCH_HOST") + .unwrap_or("localhost:9200".to_string()); +let auth_mode = env::var("MEM_AUTH_MODE") + .unwrap_or("none".to_string()); + +// Initialize clients with these URIs +let llm_client = LlmClient::new(llm_endpoint)?; +let search_client = OpenSearchClient::new(opensearch_host)?; +``` + +--- + +## Summary + +| Aspect | Production | Local | +|--------|-----------|-------| +| **Config File** | `config.yaml` | `config.local.yaml` | +| **Encryption** | SOPS (Age) | Plaintext | +| **Service URIs** | Cluster-internal DNS | External HTTPS | +| **Auth Mode** | JWT (Authentik) | None (disabled) | +| **Rate Limits** | 100/1000 | 1000/10000 | +| **Deploy** | `kubectl apply -k k8s/app/` | `kubectl apply -f config.local.yaml` | diff --git a/k8s/app/config.local.yaml b/k8s/app/config.local.yaml new file mode 100644 index 0000000..c6639d2 --- /dev/null +++ b/k8s/app/config.local.yaml @@ -0,0 +1,47 @@ +# Local/Development configuration (plaintext, external URLs via ingress) +# Use this instead of config.yaml for local testing +# kubectl apply -f config.local.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: poimen-memory-config + namespace: poimen + labels: + app.kubernetes.io/name: poimen-memory + app.kubernetes.io/component: config +data: + # Auth mode: jwt | apikey | none (disabled for local testing) + MEM_AUTH_MODE: "none" + + # Rate limiting (higher for testing) + MEM_RATE_LIMIT_INGEST: "1000" + MEM_RATE_LIMIT_QUERY: "10000" + MEM_IDEMPOTENCY_TTL_SECS: "86400" + + # Embeddings + MEM_EMBEDDING_BATCH_SIZE: "32" + + # Downstream services - external URLs via ingress + + # LLM Service (via api.riotpiao.com ingress) + LLM_ENDPOINT: "https://api.riotpiao.com/v1/chat/completions" + LLM_API_BASE: "https://api.riotpiao.com/v1" + LLM_MODEL: "qwen:7b" + LLM_TIMEOUT_SECS: "60" + ENABLE_LLM_EXTRACTION: "true" + + # OpenSearch (via ingress) + OPENSEARCH_HOST: "opensearch.riotpiao.com:443" + OPENSEARCH_SCHEME: "https" + OPENSEARCH_VERIFY_CERTS: "true" + + # Authentik (via ingress - optional for local) + AUTHENTIK_ISSUER: "https://authentik.riotpiao.com/application/o/poimen/" + AUTHENTIK_VERIFY_SSL: "true" + + # Temporal (via ingress) + TEMPORAL_ENDPOINT: "temporal.riotpiao.com:443" + TEMPORAL_NAMESPACE: "poimen" + + # API Gateway (via ingress) + GATEWAY_URL: "https://api.riotpiao.com" diff --git a/k8s/app/config.yaml b/k8s/app/config.yaml index cccfae3..7778215 100644 --- a/k8s/app/config.yaml +++ b/k8s/app/config.yaml @@ -1,5 +1,7 @@ -# Non-sensitive environment variables for poimen-memory -# Change these without redeploying secrets. +# Production environment configuration for poimen-memory +# All services use cluster-internal DNS names +# This file is encrypted with SOPS in production +# For local dev, use plaintext version with external URLs apiVersion: v1 kind: ConfigMap metadata: @@ -9,19 +11,39 @@ metadata: app.kubernetes.io/name: poimen-memory app.kubernetes.io/component: config data: - # Auth mode: jwt | apikey - MEM_AUTH_MODE: "none" + # Auth mode: jwt | apikey | none + MEM_AUTH_MODE: "jwt" + # Rate limiting MEM_RATE_LIMIT_INGEST: "100" MEM_RATE_LIMIT_QUERY: "1000" MEM_IDEMPOTENCY_TTL_SECS: "86400" + # Embeddings MEM_EMBEDDING_BATCH_SIZE: "32" - # OpenSearch - OPENSEARCH_HOST: "opensearch.poimen.svc.cluster.local:9200" - # Obsidian - # LLM Configuration (for entity extraction) - LLM_ENDPOINT: "http://api-internal.riotpiao.com:8000/v1/chat/completions" - LLM_MODEL: "qwen:7b" + + # Downstream services - read by application from ENV + # Internal cluster DNS (prod) / external URLs (local) + + # LLM Service (entity extraction, fact extraction) + LLM_ENDPOINT: "http://reasoning-predictor.llm-serving.svc.cluster.local:8000/v1/chat/completions" + LLM_API_BASE: "http://reasoning-predictor.llm-serving.svc.cluster.local:8000/v1" + LLM_MODEL: "ornith:35b" LLM_TIMEOUT_SECS: "30" ENABLE_LLM_EXTRACTION: "true" + + # OpenSearch (vector store, BM25 retrieval) + OPENSEARCH_HOST: "opensearch.poimen.svc.cluster.local:9200" + OPENSEARCH_SCHEME: "http" + OPENSEARCH_VERIFY_CERTS: "false" + + # Authentik (OIDC provider) + AUTHENTIK_ISSUER: "https://authentik.auth.svc.cluster.local:9443/application/o/poimen/" + AUTHENTIK_VERIFY_SSL: "false" + + # Temporal (workflow orchestration - future) + TEMPORAL_ENDPOINT: "temporal-frontend.temporal.svc.cluster.local:7233" + TEMPORAL_NAMESPACE: "poimen" + + # API Gateway (external queue, route optimization - future) + GATEWAY_URL: "http://api-gw.poimen.svc.cluster.local:8080" diff --git a/k8s/app/deployment.yaml b/k8s/app/deployment.yaml index 28a9239..802cd8c 100644 --- a/k8s/app/deployment.yaml +++ b/k8s/app/deployment.yaml @@ -61,20 +61,12 @@ spec: - name: DATABASE_URL value: "postgresql://$(DATABASE_USER):$(DATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(DATABASE_NAME)?sslmode=disable" - # LLM via api.riotpiao.com (Authentik JWT auth) - - name: LLM_ENDPOINT - value: "https://api.riotpiao.com/v1/chat/completions" - - name: LLM_API_BASE - value: "https://api.riotpiao.com/v1" - - name: LLM_MODEL - value: "ornith:35b" - + # All downstream service URIs read from ConfigMap + # (LLM_ENDPOINT, LLM_API_BASE, LLM_MODEL, OPENSEARCH_HOST, etc.) + # These are injected via envFrom below + # Authentik service account (memory-agent-oidc secret) - - name: AUTHENTIK_ISSUER - valueFrom: - secretKeyRef: - name: memory-agent-oidc - key: ISSUER + # Only needed if MEM_AUTH_MODE=jwt in ConfigMap - name: AUTHENTIK_CLIENT_ID valueFrom: secretKeyRef: @@ -102,6 +94,7 @@ spec: - name: MEM_HOME value: "/tmp" envFrom: + # ConfigMap with all service URIs (prod: encrypted, local: plaintext) - configMapRef: name: poimen-memory-config command: ["/app/mem"] diff --git a/k8s/app/kustomization.yaml b/k8s/app/kustomization.yaml index e097765..6e0eed2 100644 --- a/k8s/app/kustomization.yaml +++ b/k8s/app/kustomization.yaml @@ -1,13 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: poimen + resources: - # vault-pvc.yaml removed — memory service uses pgvector, not local storage - deployment.yaml - service.yaml - - config.yaml - # obsidian.yaml retired — reference docs now via memory graph - # Legacy secret managed separately - # - secrets.yaml + - config.yaml # Production config (SOPS-encrypted) + # config.local.yaml # Optional: plaintext local/dev overrides + generators: - secret-generator.yaml -- 2.54.0 From 9b3dc839bf966f78772cd290e29531eb12ffad5b Mon Sep 17 00:00:00 2001 From: rock Date: Sun, 13 Sep 2026 11:16:11 +0900 Subject: [PATCH 6/8] local dev: .env for local development (gitignored) - .env.example: template with all service URIs for local development - LOCAL_DEV.md: guide for running poimen locally - Production: config.yaml (SOPS-encrypted K8s ConfigMap) - Local: .env file (gitignored, never committed) - Application reads from ENV in both cases (K8s ConfigMap + local dotenv) - Simplifies prod/dev: same code, different config sources --- .env.example | 50 +++++++++++++++++++++++ LOCAL_DEV.md | 84 ++++++++++++++++++++++++++++++++++++++ k8s/app/kustomization.yaml | 1 - 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 LOCAL_DEV.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7fc2d86 --- /dev/null +++ b/.env.example @@ -0,0 +1,50 @@ +# Local development environment (.env file) +# Copy to .env and fill in your local/dev URLs +# .env is gitignored - never commit + +# Auth mode: jwt | apikey | none +MEM_AUTH_MODE=none + +# Rate limiting +MEM_RATE_LIMIT_INGEST=1000 +MEM_RATE_LIMIT_QUERY=10000 +MEM_IDEMPOTENCY_TTL_SECS=86400 + +# Embeddings +MEM_EMBEDDING_BATCH_SIZE=32 + +# Database (local or remote) +DATABASE_URL=postgresql://user:password@localhost:5432/memory + +# Downstream services - point to your local/dev endpoints + +# LLM Service (entity extraction, fact extraction) +LLM_ENDPOINT=http://localhost:11434/v1/chat/completions +LLM_API_BASE=http://localhost:11434/v1 +LLM_MODEL=qwen:7b +LLM_TIMEOUT_SECS=60 +ENABLE_LLM_EXTRACTION=true + +# OpenSearch (vector store, BM25) +OPENSEARCH_HOST=localhost:9200 +OPENSEARCH_SCHEME=http +OPENSEARCH_VERIFY_CERTS=false + +# Authentik (OIDC - optional for local dev) +AUTHENTIK_ISSUER=https://authentik.riotpiao.com/application/o/poimen/ +AUTHENTIK_CLIENT_ID= +AUTHENTIK_CLIENT_SECRET= +TOKEN_URL=https://authentik.riotpiao.com/application/o/token/ +AUTHENTIK_VERIFY_SSL=false + +# Temporal (workflow orchestration - future) +TEMPORAL_ENDPOINT=localhost:7233 +TEMPORAL_NAMESPACE=poimen + +# API Gateway (route optimization - future) +GATEWAY_URL=http://localhost:8080 + +# Server config +MEM_PORT=8080 +MEM_API_KEY=test-key +MEM_HOME=/tmp diff --git a/LOCAL_DEV.md b/LOCAL_DEV.md new file mode 100644 index 0000000..c11b6e4 --- /dev/null +++ b/LOCAL_DEV.md @@ -0,0 +1,84 @@ +# Local Development Setup + +Running poimen-memory locally for development. + +## Quick Start + +1. **Copy env template**: + ```bash + cp .env.example .env + ``` + +2. **Edit `.env`** with your local endpoints: + ```bash + # Edit .env with your local/dev service URLs + # Example: LLM service on localhost:11434, OpenSearch on localhost:9200 + ``` + +3. **Run the service**: + ```bash + cargo run --release -- serve --port 8080 + ``` + +The application loads configuration from `.env` (via `dotenvy` or similar). + +## `.env` File + +**Location**: Project root (`.env`) +**Status**: Gitignored - never committed +**Template**: `.env.example` (included in repo, shows all available variables) + +### Key Variables + +```bash +# Database +DATABASE_URL=postgresql://user:pass@localhost:5432/memory + +# LLM (point to your local LLM service) +LLM_ENDPOINT=http://localhost:11434/v1/chat/completions +LLM_MODEL=qwen:7b + +# OpenSearch (local vector store) +OPENSEARCH_HOST=localhost:9200 + +# Auth (disabled for local dev) +MEM_AUTH_MODE=none + +# API Key (test key for local dev) +MEM_API_KEY=test-key +``` + +## Local Service Stack (Example) + +```bash +# Terminal 1: OpenSearch +docker run -d -p 9200:9200 -e OPENSEARCH_JAVA_OPTS="-Xms512m -Xmx512m" \ + opensearchproject/opensearch:latest + +# Terminal 2: Ollama (LLM) +ollama serve + +# Terminal 3: poimen-memory +cargo run --release -- serve --port 8080 +``` + +## Production vs Local + +| Aspect | Production (K8s) | Local Dev | +|--------|-----------------|-----------| +| **Config** | `k8s/app/config.yaml` (SOPS-encrypted) | `.env` (gitignored) | +| **Injection** | ConfigMap via `envFrom:` | dotenv via `dotenvy` crate | +| **Services** | Cluster-internal DNS | localhost/127.0.0.1 | +| **Auth** | JWT (Authentik) | None (disabled) | +| **Commit?** | Yes (encrypted) | No (gitignored) | + +## Switching to Production Config + +To run against production services (not recommended locally): +1. Edit `.env` with production URLs +2. Set credentials appropriately +3. Ensure network access to production services + +--- + +See `.env.example` for all available environment variables. diff --git a/k8s/app/kustomization.yaml b/k8s/app/kustomization.yaml index 6e0eed2..7ec3af7 100644 --- a/k8s/app/kustomization.yaml +++ b/k8s/app/kustomization.yaml @@ -6,7 +6,6 @@ resources: - deployment.yaml - service.yaml - config.yaml # Production config (SOPS-encrypted) - # config.local.yaml # Optional: plaintext local/dev overrides generators: - secret-generator.yaml -- 2.54.0 From 346a615535d4cba35d019f496c96dd6309e7dd7a Mon Sep 17 00:00:00 2001 From: rock Date: Sun, 13 Sep 2026 14:13:18 +0900 Subject: [PATCH 7/8] fix: docker build out-of-disk issue - aggressive cleanup - .dockerignore: exclude build/cache/docs to reduce build context (59 lines) - Dockerfile: add aggressive cleanup (cache, registry, git) after cargo build - Dockerfile: use --locked flag for reproducible builds - build.yaml: add pre-build disk cleanup (docker + cargo cache) - build.yaml: clean cargo before Docker build to free space - build.yaml: show disk usage before/after for debugging Fixes 'No space left on device' error during Docker build in runner --- .dockerignore | 59 ++++++++++++++++++++++++++++++++----- .gitea/workflows/build.yaml | 22 ++++++++++++-- Dockerfile | 17 ++++++++--- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/.dockerignore b/.dockerignore index 1920e5f..aec4b85 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,55 @@ +# Git .git .gitignore +.gitattributes + +# CI/CD +.github +.gitea +.gitlab-ci.yml + +# Kubernetes +k8s/ +helm/ + +# Documentation *.md -__pycache__ -*.pyc -.env.local -.venv -venv/ -.pytest_cache -.coverage -htmlcov +docs/ + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS .DS_Store +Thumbs.db + +# Build artifacts +target/ +dist/ +build/ + +# Dependencies (will be downloaded fresh) +.cargo/ +Cargo.lock.bak + +# Testing +.coverage +coverage/ + +# Secrets +.env +.env.local +.env.*.local + +# Archives +*.tar +*.tar.gz +*.zip + +# Node (if any) +node_modules/ +*.log diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 0857b41..d03e7db 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -18,6 +18,15 @@ jobs: name: CI runs-on: rust steps: + - name: Clean disk space (runner GC) + run: | + df -h / + echo "Cleaning docker, cargo cache..." + docker system prune -af --volumes || true + rm -rf ~/.cargo/registry/cache ~/.cargo/registry/index ~/.cargo/git || true + rm -rf /tmp/* || true + df -h / + - name: Install Node.js and Docker run: | apt-get update @@ -48,6 +57,12 @@ jobs: REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} + - name: Clean cargo before Docker build + run: | + cargo clean || true + rm -rf ~/.cargo/registry/cache ~/.cargo/registry/index ~/.cargo/git || true + df -h / + - name: Build and push Docker image (SHA tag only) run: | docker build --no-cache --progress=plain \ @@ -56,5 +71,8 @@ jobs: docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}" echo "Pushed: ${IMAGE}:${{ steps.sha.outputs.short_sha }}" - - name: Prune unused images - run: docker image prune -a --force 2>&1 | tail -3 || true + - name: Prune unused images and cleanup + run: | + docker image prune -a --force 2>&1 | tail -3 || true + cargo clean || true + df -h / diff --git a/Dockerfile b/Dockerfile index 2b3bca7..07a2041 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,23 @@ FROM rust:1-bookworm as builder WORKDIR /build +# Build settings +ENV SQLX_OFFLINE=true + # Copy source COPY . . -# Build the mem binary (offline sqlx - uses .sqlx/ cache) -ENV SQLX_OFFLINE=true -RUN cargo build --release -p mem-cli && \ +# Build release binary with space-efficient cleanup +RUN cargo build --release -p mem-cli --locked && \ strip target/release/mem && \ - rm -rf target/release/deps target/release/build target/release/incremental target/release/.fingerprint + # Aggressive cleanup to free disk space + rm -rf target/release/deps && \ + rm -rf target/release/build && \ + rm -rf target/release/incremental && \ + rm -rf target/release/.fingerprint && \ + rm -rf .cargo/registry/cache && \ + rm -rf .cargo/registry/index && \ + rm -rf .cargo/git # Stage 2: Runtime FROM debian:bookworm-slim -- 2.54.0 From bcda5617b596d5b9b0e9ed5fae6bb2e4c46ff4e0 Mon Sep 17 00:00:00 2001 From: rock Date: Sun, 13 Sep 2026 14:14:28 +0900 Subject: [PATCH 8/8] infra: add CronJob for runner disk cleanup - Runs daily at 2 AM to clean Docker, Cargo cache, /tmp - Prevents 'no space left on device' errors in CI builds - Deploy to homelab: kubectl apply -f k8s/infra/runner-cleanup-cronjob.yaml - Adjust namespace and node selectors for your environment --- k8s/infra/runner-cleanup-cronjob.yaml | 121 ++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 k8s/infra/runner-cleanup-cronjob.yaml diff --git a/k8s/infra/runner-cleanup-cronjob.yaml b/k8s/infra/runner-cleanup-cronjob.yaml new file mode 100644 index 0000000..28e9017 --- /dev/null +++ b/k8s/infra/runner-cleanup-cronjob.yaml @@ -0,0 +1,121 @@ +# CronJob to periodically clean Gitea Actions runner disk space +# Prevents "no space left on device" errors during Docker builds +# Deploy to: kubectl apply -f k8s/infra/runner-cleanup-cronjob.yaml + +apiVersion: batch/v1 +kind: CronJob +metadata: + name: runner-disk-cleanup + namespace: ci # Adjust to your runner namespace + labels: + app: runner-cleanup +spec: + # Run daily at 2 AM + schedule: "0 2 * * *" + + # Keep last 3 successful jobs + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + + jobTemplate: + spec: + template: + metadata: + labels: + app: runner-cleanup + spec: + serviceAccountName: runner-cleanup + + # Run on node with Gitea Actions runner + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - runner-node # Adjust to your runner node name + + containers: + - name: cleanup + image: docker:24 + securityContext: + privileged: true # Needed to access Docker daemon + command: + - /bin/sh + - -c + - | + echo "=== Runner disk cleanup at $(date) ===" + + df -h / + echo "" + + echo "Cleaning Docker..." + docker system prune -af --volumes 2>&1 | tail -5 + + echo "" + echo "Cleaning Cargo cache..." + rm -rf /root/.cargo/registry/cache 2>/dev/null + rm -rf /root/.cargo/registry/index 2>/dev/null + rm -rf /root/.cargo/git 2>/dev/null + + echo "" + echo "Cleaning /tmp..." + rm -rf /tmp/* 2>/dev/null + + echo "" + echo "Disk after cleanup:" + df -h / + + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + - name: runner-home + mountPath: /root + + volumes: + # Access Docker daemon on host + - name: docker-sock + hostPath: + path: /var/run/docker.sock + # Access runner home directory + - name: runner-home + hostPath: + path: /home/runner # Adjust to your runner home path + + restartPolicy: OnFailure + +--- +# ServiceAccount for cleanup job +apiVersion: v1 +kind: ServiceAccount +metadata: + name: runner-cleanup + namespace: ci + +--- +# Role for cleanup job +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: runner-cleanup +rules: + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list"] + +--- +# RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: runner-cleanup +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: runner-cleanup +subjects: + - kind: ServiceAccount + name: runner-cleanup + namespace: ci -- 2.54.0