Skip to content

Commit cf5ea2f

Browse files
committed
feat(keep): deploy
1 parent ab9cf56 commit cf5ea2f

8 files changed

Lines changed: 471 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1.json
3+
apiVersion: external-secrets.io/v1
4+
kind: ExternalSecret
5+
metadata:
6+
name: keep
7+
spec:
8+
secretStoreRef:
9+
kind: ClusterSecretStore
10+
name: onepassword
11+
target:
12+
name: keep
13+
template:
14+
engineVersion: v2
15+
data:
16+
# Frontend (next-auth JWT signing)
17+
NEXTAUTH_SECRET: "{{ .NEXTAUTH_SECRET }}"
18+
# Backend → Soketi (Pusher protocol; APP_KEY is also exposed to browsers via /api/config)
19+
PUSHER_APP_ID: "{{ .PUSHER_APP_ID }}"
20+
PUSHER_APP_KEY: "{{ .PUSHER_APP_KEY }}"
21+
PUSHER_APP_SECRET: "{{ .PUSHER_APP_SECRET }}"
22+
# Soketi (must mirror PUSHER_APP_*)
23+
SOKETI_DEFAULT_APP_ID: "{{ .PUSHER_APP_ID }}"
24+
SOKETI_DEFAULT_APP_KEY: "{{ .PUSHER_APP_KEY }}"
25+
SOKETI_DEFAULT_APP_SECRET: "{{ .PUSHER_APP_SECRET }}"
26+
dataFrom:
27+
- extract:
28+
key: keep
29+
---
30+
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1.json
31+
apiVersion: external-secrets.io/v1
32+
kind: ExternalSecret
33+
metadata:
34+
name: keep-db
35+
spec:
36+
refreshInterval: "0"
37+
secretStoreRef:
38+
kind: ClusterSecretStore
39+
name: onepassword
40+
target:
41+
name: keep-db
42+
template:
43+
engineVersion: v2
44+
data:
45+
# Backend SQLAlchemy connection (psycopg2 driver)
46+
DATABASE_CONNECTION_STRING: "postgresql+psycopg2://keep:{{ .DB_PASSWORD }}@pg18vc-rw.database.svc.cluster.local:5432/keep"
47+
# postgres-init initContainer
48+
INIT_POSTGRES_DBNAME: keep
49+
INIT_POSTGRES_HOST: pg18vc-rw.database.svc.cluster.local
50+
INIT_POSTGRES_USER: keep
51+
INIT_POSTGRES_PASS: "{{ .DB_PASSWORD }}"
52+
dataFrom:
53+
- sourceRef:
54+
generatorRef:
55+
apiVersion: generators.external-secrets.io/v1alpha1
56+
kind: Password
57+
name: password32
58+
rewrite:
59+
- regexp:
60+
source: "password"
61+
target: "DB_PASSWORD"
62+
---
63+
# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1.json
64+
apiVersion: external-secrets.io/v1
65+
kind: ExternalSecret
66+
metadata:
67+
name: keep-initdb
68+
spec:
69+
secretStoreRef:
70+
kind: ClusterSecretStore
71+
name: onepassword
72+
target:
73+
name: keep-initdb
74+
data:
75+
- secretKey: INIT_POSTGRES_SUPER_PASS
76+
remoteRef:
77+
key: cnpg-pg18vc/password
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
---
2+
# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/helmrepository-source-v1.json
3+
apiVersion: source.toolkit.fluxcd.io/v1
4+
kind: HelmRepository
5+
metadata:
6+
name: keephq
7+
spec:
8+
interval: 1h
9+
url: https://keephq.github.io/helm-charts
10+
---
11+
# yaml-language-server: $schema=https://raw.githubusercontent.com/fluxcd-community/flux2-schemas/main/helmrelease-helm-v2.json
12+
apiVersion: helm.toolkit.fluxcd.io/v2
13+
kind: HelmRelease
14+
metadata:
15+
name: keep
16+
spec:
17+
interval: 1h
18+
chart:
19+
spec:
20+
chart: keep
21+
version: 0.1.95
22+
sourceRef:
23+
kind: HelmRepository
24+
name: keephq
25+
driftDetection:
26+
mode: enabled
27+
install:
28+
remediation:
29+
retries: -1
30+
upgrade:
31+
cleanupOnFail: true
32+
remediation:
33+
retries: 3
34+
values:
35+
fullnameOverride: keep
36+
# Disable bundled nginx Ingress — HTTPRoute is provided alongside this release.
37+
global:
38+
ingress:
39+
enabled: false
40+
# Disable bundled MySQL — backend points at the external CNPG cluster.
41+
database:
42+
enabled: false
43+
44+
backend:
45+
enabled: true
46+
replicaCount: 1
47+
podAnnotations:
48+
reloader.stakater.com/auto: "true"
49+
prometheus.io/scrape: "true"
50+
prometheus.io/path: /metrics
51+
prometheus.io/port: "8080"
52+
# Pod-level securityContext — required by `restricted` PSS on this namespace.
53+
# The keep-api Dockerfile sets USER keep (uid 1000).
54+
podSecurityContext:
55+
runAsNonRoot: true
56+
runAsUser: 1000
57+
runAsGroup: 1000
58+
fsGroup: 1000
59+
fsGroupChangePolicy: OnRootMismatch
60+
seccompProfile:
61+
type: RuntimeDefault
62+
securityContext:
63+
allowPrivilegeEscalation: false
64+
readOnlyRootFilesystem: true
65+
capabilities:
66+
drop: ["ALL"]
67+
service:
68+
type: ClusterIP
69+
port: 8080
70+
databaseConnectionStringFromSecret:
71+
enabled: true
72+
secretName: keep-db
73+
secretKey: DATABASE_CONNECTION_STRING
74+
# Disabled: the chart's auto-generated `wait-for-database` initContainer uses
75+
# busybox with no securityContext, which violates the namespace's `restricted` PSS.
76+
# The `init-db` postgres-init initContainer below already implicitly waits — it
77+
# fails (k8s retries) until CNPG is reachable.
78+
waitForDatabase:
79+
enabled: false
80+
env:
81+
# Replaces chart defaults entirely. DATABASE_CONNECTION_STRING is injected
82+
# by the chart from the keep-db secret via databaseConnectionStringFromSecret.
83+
# NEXTAUTH_SECRET / PUSHER_APP_* come in via envFromSecret: keep below.
84+
- name: AUTH_TYPE
85+
value: NO_AUTH
86+
- name: PORT
87+
value: "8080"
88+
# k8s secret manager — chart auto-creates the Role+RoleBinding (see
89+
# templates/role-secret-manager.yaml + role-binding-secret-manager.yaml)
90+
# in the release namespace, scoped to the keep ServiceAccount with
91+
# verbs [create, delete, get, list, patch] on secrets. K8S_NAMESPACE is
92+
# set automatically by the chart via fieldRef: metadata.namespace.
93+
- name: SECRET_MANAGER_TYPE
94+
value: k8s
95+
- name: KEEP_METRICS
96+
value: "true"
97+
- name: PROMETHEUS_MULTIPROC_DIR
98+
value: /tmp/prometheus
99+
# Public-facing API URL (used in webhooks, email links, etc.).
100+
# Overrides the chart's keep.keepApiUrl helper, which would otherwise fall back
101+
# to http://localhost:3000/v2 when global.ingress.enabled=false.
102+
- name: KEEP_API_URL
103+
value: https://keep.kantai.xyz/v2
104+
# Pusher: backend publishes to in-cluster Soketi service
105+
- name: PUSHER_HOST
106+
value: keep-websocket
107+
- name: PUSHER_PORT
108+
value: "6001"
109+
# gunicorn writes PIDs / sockets to XDG_RUNTIME_DIR — /run is a tmpfs (see extraVolumes)
110+
- name: XDG_RUNTIME_DIR
111+
value: /run
112+
envFromSecret: keep
113+
# The chart's extraInitContainers loop in templates/backend.yaml only renders
114+
# `imagePullPolicy`, `command`, `args`, `env`, `volumeMounts`, `restartPolicy`
115+
# — `envFrom` is silently dropped. We inject it via the postRenderer below.
116+
extraInitContainers:
117+
- name: init-db
118+
image: ghcr.io/home-operations/postgres-init:18.3.0@sha256:6fa1f331cddd2eb0b6afa7b8d3685c864127a81ab01c3d9400bc3ff5263a51cf
119+
extraVolumes:
120+
- name: tmp
121+
emptyDir:
122+
sizeLimit: 256Mi
123+
- name: run
124+
emptyDir:
125+
medium: Memory
126+
sizeLimit: 16Mi
127+
extraVolumeMounts:
128+
- name: tmp
129+
mountPath: /tmp
130+
- name: run
131+
mountPath: /run
132+
resources:
133+
requests:
134+
cpu: 50m
135+
memory: 256Mi
136+
limits:
137+
memory: 1Gi
138+
139+
frontend:
140+
enabled: true
141+
replicaCount: 1
142+
podAnnotations:
143+
reloader.stakater.com/auto: "true"
144+
# The keep-ui Dockerfile sets USER nextjs (uid 1001).
145+
podSecurityContext:
146+
runAsNonRoot: true
147+
runAsUser: 1001
148+
runAsGroup: 1001
149+
fsGroup: 1001
150+
fsGroupChangePolicy: OnRootMismatch
151+
seccompProfile:
152+
type: RuntimeDefault
153+
securityContext:
154+
allowPrivilegeEscalation: false
155+
readOnlyRootFilesystem: true
156+
capabilities:
157+
drop: ["ALL"]
158+
service:
159+
type: ClusterIP
160+
port: 3000
161+
env:
162+
# The chart's frontend pod template recognises four well-known keys via findEnvVar
163+
# (NEXTAUTH_URL, API_URL, API_URL_CLIENT, PUSHER_HOST) and renders them itself.
164+
# NEXTAUTH_SECRET / PUSHER_APP_KEY come in via envFromSecret: keep below.
165+
- name: AUTH_TYPE
166+
value: NO_AUTH
167+
- name: ENV
168+
value: production
169+
- name: NODE_ENV
170+
value: production
171+
- name: HOSTNAME
172+
value: 0.0.0.0
173+
# next-auth issue 600 workaround — the chart sets this by default; keep it.
174+
- name: VERCEL
175+
value: "1"
176+
- name: FRIGADE_DISABLED
177+
value: "true"
178+
- name: POSTHOG_DISABLED
179+
value: "true"
180+
# Public NEXTAUTH base URL (no `/api` suffix — next-auth appends it).
181+
- name: NEXTAUTH_URL
182+
value: https://keep.kantai.xyz
183+
# Server-side fetch from the frontend pod → backend service.
184+
- name: API_URL
185+
value: http://keep-backend:8080
186+
# Browser-side fetch path (relative to current host); matches HTTPRoute /v2 prefix.
187+
- name: API_URL_CLIENT
188+
value: /v2
189+
# Relative pusher path → keep-ui's usePusher() treats it as wsPath on window.location.host.
190+
- name: PUSHER_HOST
191+
value: /websocket
192+
- name: PUSHER_PORT
193+
value: "443"
194+
envFromSecret: keep
195+
extraVolumes:
196+
- name: tmp
197+
emptyDir:
198+
sizeLimit: 256Mi
199+
- name: next-cache
200+
emptyDir:
201+
sizeLimit: 1Gi
202+
extraVolumeMounts:
203+
- name: tmp
204+
mountPath: /tmp
205+
- name: next-cache
206+
mountPath: /app/.next/cache
207+
resources:
208+
requests:
209+
cpu: 25m
210+
memory: 256Mi
211+
limits:
212+
memory: 1Gi
213+
214+
websocket:
215+
enabled: true
216+
replicaCount: 1
217+
podAnnotations:
218+
reloader.stakater.com/auto: "true"
219+
# Soketi's Dockerfile.debian has no USER directive (runs as root by default).
220+
# Soketi listens on 6001 (>1024) and only writes /state (emptyDir), so any uid works.
221+
podSecurityContext:
222+
runAsNonRoot: true
223+
runAsUser: 65534
224+
runAsGroup: 65534
225+
fsGroup: 65534
226+
fsGroupChangePolicy: OnRootMismatch
227+
seccompProfile:
228+
type: RuntimeDefault
229+
securityContext:
230+
allowPrivilegeEscalation: false
231+
readOnlyRootFilesystem: true
232+
capabilities:
233+
drop: ["ALL"]
234+
service:
235+
type: ClusterIP
236+
port: 6001
237+
# SOKETI_DEFAULT_APP_ID/KEY/SECRET are injected from the keep secret via the
238+
# postRenderer below — the chart's websocket template doesn't expose envFromSecret.
239+
env:
240+
- name: SOKETI_HOST
241+
value: 0.0.0.0
242+
- name: SOKETI_DEBUG
243+
value: "0"
244+
- name: SOKETI_USER_AUTHENTICATION_TIMEOUT
245+
value: "3000"
246+
resources:
247+
requests:
248+
cpu: 25m
249+
memory: 64Mi
250+
limits:
251+
memory: 256Mi
252+
253+
postRenderers:
254+
- kustomize:
255+
patches:
256+
# Inject the shared `keep` secret into the Soketi container so SOKETI_DEFAULT_APP_*
257+
# match the backend's PUSHER_APP_*. The chart's websocket-server.yaml template
258+
# only ranges values.websocket.env (no envFrom support), so this is the cleanest fix.
259+
- target:
260+
kind: Deployment
261+
name: keep-websocket
262+
patch: |
263+
- op: add
264+
path: /spec/template/spec/containers/0/envFrom
265+
value:
266+
- secretRef:
267+
name: keep
268+
# The chart's `extraInitContainers` loop in templates/backend.yaml renders
269+
# neither `securityContext` nor `envFrom`, so inject both onto init-db here.
270+
# postgres-init's Dockerfile sets USER nobody:nogroup, but the pod-level
271+
# runAsUser=1000 overrides it; either works since postgres-init only opens
272+
# a libpq connection and needs no special filesystem access.
273+
- target:
274+
kind: Deployment
275+
name: keep-backend
276+
patch: |
277+
- op: add
278+
path: /spec/template/spec/initContainers/0/securityContext
279+
value:
280+
allowPrivilegeEscalation: false
281+
readOnlyRootFilesystem: true
282+
capabilities:
283+
drop: ["ALL"]
284+
- op: add
285+
path: /spec/template/spec/initContainers/0/envFrom
286+
value:
287+
- secretRef:
288+
name: keep-db
289+
- secretRef:
290+
name: keep-initdb

0 commit comments

Comments
 (0)