@podosoft/podokit 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +20 -0
  2. package/dist/add.d.ts +4 -0
  3. package/dist/add.js +59 -1
  4. package/dist/create.js +8 -1
  5. package/dist/deploy-profile.d.ts +92 -0
  6. package/dist/deploy-profile.js +603 -0
  7. package/dist/deploy-render.d.ts +18 -0
  8. package/dist/deploy-render.js +651 -0
  9. package/dist/deploy.d.ts +65 -0
  10. package/dist/deploy.js +851 -0
  11. package/dist/dev.d.ts +1 -0
  12. package/dist/dev.js +29 -5
  13. package/dist/index.d.ts +8 -0
  14. package/dist/index.js +147 -1
  15. package/dist/lib.d.ts +6 -0
  16. package/dist/lib.js +18 -1
  17. package/dist/lockfile.js +3 -0
  18. package/dist/remove.js +7 -0
  19. package/dist/templates/fullstack-nest-svelte/AGENTS.md +1 -0
  20. package/dist/templates/fullstack-nest-svelte/README.md +19 -8
  21. package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +2 -2
  22. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -0
  23. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/redis.connection.spec.ts +53 -0
  24. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/redis.connection.ts +69 -0
  25. package/dist/templates/fullstack-nest-svelte/apps/api/src/database/migrate.ts +16 -0
  26. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +19 -4
  27. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.module.ts +5 -1
  28. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/readiness.service.spec.ts +22 -0
  29. package/dist/templates/fullstack-nest-svelte/apps/api/src/health/readiness.service.ts +33 -0
  30. package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +2 -2
  31. package/dist/templates/fullstack-nest-svelte/apps/web/src/app.html +2 -0
  32. package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +3 -5
  33. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/SKILL.md +20 -0
  34. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/agents/openai.yaml +4 -0
  35. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/profile.md +37 -0
  36. package/dist/templates/fullstack-nest-svelte/dot-agents/skills/podokit-deploy/references/rollback.md +18 -0
  37. package/dist/templates/fullstack-nest-svelte/dot-claude/skills/podokit-deploy/SKILL.md +8 -0
  38. package/dist/templates/fullstack-nest-svelte/dot-env.example +6 -0
  39. package/dist/templates/fullstack-nest-svelte/infra/k3s/api-deployment.yaml +1 -1
  40. package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +0 -4
  41. package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +6 -4
  42. package/dist/templates/fullstack-nest-svelte/tests/playwright.extensions.ts +44 -0
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +35 -4
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +33 -4
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +6 -0
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +18 -10
  47. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +11 -6
  48. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +18 -8
  49. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +15 -9
  50. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +1 -0
  51. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +83 -0
  52. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +13 -10
  53. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +69 -3
  54. package/dist/templates/modules/admin-dashboard/files/tests/seed.teardown.ts +46 -0
  55. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +13 -0
  56. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +3 -6
  57. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +34 -28
  58. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +29 -39
  59. package/dist/templates/modules/admin-dashboard/module.manifest.json +1 -1
  60. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key-verifier.spec.ts +36 -0
  61. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key-verifier.ts +20 -0
  62. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.spec.ts +49 -0
  63. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.ts +3 -11
  64. package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.module.ts +3 -1
  65. package/dist/templates/modules/api-key-auth/module.manifest.json +1 -1
  66. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +32 -0
  67. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +35 -0
  68. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +3 -2
  69. package/dist/templates/modules/auth/files/apps/api/src/health/health.controller.ts +19 -4
  70. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/queue.ts +4 -5
  71. package/dist/templates/modules/bullmq/module.manifest.json +4 -1
  72. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +36 -0
  73. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.ts +26 -5
  74. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.spec.ts +35 -0
  75. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +21 -2
  76. package/dist/templates/modules/object-storage-s3/module.manifest.json +3 -0
  77. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +85 -0
  78. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +110 -0
  79. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +132 -0
  80. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +100 -0
  81. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.integration.spec.ts +91 -0
  82. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +228 -0
  83. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +97 -38
  84. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +11 -6
  85. package/dist/templates/modules/rate-limit/module.manifest.json +23 -9
  86. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.spec.ts +28 -0
  87. package/dist/templates/modules/redis/files/apps/api/src/redis/redis.service.ts +52 -11
  88. package/dist/templates/modules/redis/module.manifest.json +16 -1
  89. package/dist/templates/modules/sse/files/apps/api/src/events/events.controller.ts +17 -3
  90. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +9 -2
  91. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +61 -0
  92. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +44 -4
  93. package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.redis.spec.ts +42 -0
  94. package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.spec.ts +97 -0
  95. package/dist/templates/modules/sse/files/apps/api/src/events/events.transport.ts +165 -0
  96. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +41 -0
  97. package/dist/templates/modules/sse/module.manifest.json +16 -3
  98. package/dist/templates/todo/AGENTS.md +1 -0
  99. package/dist/templates/todo/README.md +12 -8
  100. package/dist/templates/todo/apps/api/Dockerfile +2 -2
  101. package/dist/templates/todo/apps/api/package.json +1 -0
  102. package/dist/templates/todo/apps/api/src/config/redis.connection.spec.ts +53 -0
  103. package/dist/templates/todo/apps/api/src/config/redis.connection.ts +69 -0
  104. package/dist/templates/todo/apps/api/src/database/migrate.ts +16 -0
  105. package/dist/templates/todo/apps/api/src/health/health.controller.ts +19 -4
  106. package/dist/templates/todo/apps/api/src/health/health.module.ts +5 -1
  107. package/dist/templates/todo/apps/api/src/health/readiness.service.spec.ts +22 -0
  108. package/dist/templates/todo/apps/api/src/health/readiness.service.ts +33 -0
  109. package/dist/templates/todo/apps/web/Dockerfile +2 -2
  110. package/dist/templates/todo/compose.dev.yaml +3 -5
  111. package/dist/templates/todo/dot-agents/skills/podokit-deploy/SKILL.md +20 -0
  112. package/dist/templates/todo/dot-agents/skills/podokit-deploy/agents/openai.yaml +4 -0
  113. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/profile.md +28 -0
  114. package/dist/templates/todo/dot-agents/skills/podokit-deploy/references/rollback.md +5 -0
  115. package/dist/templates/todo/dot-claude/skills/podokit-deploy/SKILL.md +8 -0
  116. package/dist/templates/todo/dot-env.example +6 -0
  117. package/dist/templates/todo/infra/k3s/api-deployment.yaml +1 -1
  118. package/dist/templates/todo/infra/k3s/ingress.yaml +0 -4
  119. package/dist/templates/todo/tests/playwright.config.ts +6 -4
  120. package/dist/templates/todo/tests/playwright.extensions.ts +44 -0
  121. package/dist/update.js +17 -13
  122. package/package.json +2 -2
@@ -0,0 +1,651 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDeployment = renderDeployment;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_path_1 = require("node:path");
7
+ const deploy_profile_1 = require("./deploy-profile");
8
+ function quote(value) {
9
+ return JSON.stringify(value);
10
+ }
11
+ function digest(parts) {
12
+ const hash = (0, node_crypto_1.createHash)("sha256");
13
+ for (const part of parts)
14
+ hash.update(part).update("\0");
15
+ return `sha256:${hash.digest("hex")}`;
16
+ }
17
+ function offlineRolloutStateDigest(profile) {
18
+ const secretNames = [
19
+ profile.secrets.api.name,
20
+ ...(profile.secrets.web ? [profile.secrets.web.name] : []),
21
+ profile.secrets.imagePull,
22
+ ...(profile.exposure.tlsSecretName ? [profile.exposure.tlsSecretName] : []),
23
+ profile.dependencies.postgres.secretName,
24
+ profile.dependencies.redis.secretName,
25
+ profile.dependencies.objectStorage.secretName,
26
+ ].sort();
27
+ return digest(["offline-rollout-state", JSON.stringify(secretNames)]);
28
+ }
29
+ function podTemplateAnnotations(rolloutStateDigest, runtimeConfigDigest) {
30
+ const annotations = [
31
+ `podokit.example.com/rollout-state-checksum: ${quote(digest(["rollout-state", rolloutStateDigest]))}`,
32
+ ...(runtimeConfigDigest
33
+ ? [`podokit.example.com/runtime-config-checksum: ${quote(runtimeConfigDigest)}`]
34
+ : []),
35
+ ];
36
+ return ` annotations:
37
+ ${annotations.map((annotation) => ` ${annotation}`).join("\n")}`;
38
+ }
39
+ function storageClassLine(dependency) {
40
+ return dependency.storageClassName
41
+ ? ` storageClassName: ${quote(dependency.storageClassName)}\n`
42
+ : "";
43
+ }
44
+ function resources(profile, indent = 10) {
45
+ const pad = " ".repeat(indent);
46
+ return `${pad}resources:
47
+ ${pad} requests:
48
+ ${pad} cpu: ${quote(profile.cpuRequest)}
49
+ ${pad} memory: ${quote(profile.memoryRequest)}
50
+ ${pad} limits:
51
+ ${pad} cpu: ${quote(profile.cpuLimit)}
52
+ ${pad} memory: ${quote(profile.memoryLimit)}
53
+ `;
54
+ }
55
+ function labels(name, indent = 4) {
56
+ const pad = " ".repeat(indent);
57
+ return `${pad}app.kubernetes.io/name: ${name}
58
+ ${pad}app.kubernetes.io/managed-by: podokit
59
+ `;
60
+ }
61
+ function postgresResources(profile, image, rolloutStateDigest) {
62
+ const dependency = profile.dependencies.postgres;
63
+ if (dependency.mode !== "inCluster")
64
+ return "";
65
+ const name = `${profile.target.release}-postgres`;
66
+ return `apiVersion: v1
67
+ kind: Service
68
+ metadata:
69
+ name: ${name}
70
+ labels:
71
+ ${labels(name)}spec:
72
+ selector:
73
+ app.kubernetes.io/name: ${name}
74
+ ports:
75
+ - name: postgres
76
+ port: 5432
77
+ targetPort: 5432
78
+ ---
79
+ apiVersion: apps/v1
80
+ kind: StatefulSet
81
+ metadata:
82
+ name: ${name}
83
+ spec:
84
+ serviceName: ${name}
85
+ replicas: 1
86
+ selector:
87
+ matchLabels:
88
+ app.kubernetes.io/name: ${name}
89
+ template:
90
+ metadata:
91
+ ${podTemplateAnnotations(rolloutStateDigest)}
92
+ labels:
93
+ ${labels(name, 8)} spec:
94
+ imagePullSecrets:
95
+ - name: ${profile.secrets.imagePull}
96
+ securityContext:
97
+ fsGroup: 70
98
+ seccompProfile: { type: RuntimeDefault }
99
+ containers:
100
+ - name: postgres
101
+ image: ${quote(image ?? dependency.image)}
102
+ imagePullPolicy: IfNotPresent
103
+ env:
104
+ - name: POSTGRES_USER
105
+ valueFrom:
106
+ secretKeyRef: { name: ${dependency.secretName}, key: POSTGRES_USER }
107
+ - name: POSTGRES_PASSWORD
108
+ valueFrom:
109
+ secretKeyRef: { name: ${dependency.secretName}, key: POSTGRES_PASSWORD }
110
+ - name: POSTGRES_DB
111
+ valueFrom:
112
+ secretKeyRef: { name: ${dependency.secretName}, key: POSTGRES_DB }
113
+ ports:
114
+ - { name: postgres, containerPort: 5432 }
115
+ readinessProbe:
116
+ exec:
117
+ command: [sh, -c, 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
118
+ periodSeconds: 5
119
+ livenessProbe:
120
+ exec:
121
+ command: [sh, -c, 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
122
+ initialDelaySeconds: 30
123
+ periodSeconds: 10
124
+ resources:
125
+ requests: { cpu: "250m", memory: "512Mi" }
126
+ limits: { cpu: "2000m", memory: "2Gi" }
127
+ volumeMounts:
128
+ - { name: data, mountPath: /var/lib/postgresql/data }
129
+ volumeClaimTemplates:
130
+ - metadata:
131
+ name: data
132
+ spec:
133
+ ${storageClassLine(dependency)} accessModes: [ReadWriteOnce]
134
+ resources:
135
+ requests:
136
+ storage: ${quote(dependency.storageSize)}
137
+ `;
138
+ }
139
+ function redisResources(profile, image, rolloutStateDigest) {
140
+ const dependency = profile.dependencies.redis;
141
+ if (dependency.mode !== "inCluster")
142
+ return "";
143
+ const name = `${profile.target.release}-redis`;
144
+ return `apiVersion: v1
145
+ kind: Service
146
+ metadata:
147
+ name: ${name}
148
+ spec:
149
+ selector:
150
+ app.kubernetes.io/name: ${name}
151
+ ports:
152
+ - name: redis
153
+ port: 6379
154
+ targetPort: 6379
155
+ ---
156
+ apiVersion: apps/v1
157
+ kind: StatefulSet
158
+ metadata:
159
+ name: ${name}
160
+ spec:
161
+ serviceName: ${name}
162
+ replicas: 1
163
+ selector:
164
+ matchLabels:
165
+ app.kubernetes.io/name: ${name}
166
+ template:
167
+ metadata:
168
+ ${podTemplateAnnotations(rolloutStateDigest)}
169
+ labels:
170
+ ${labels(name, 8)} spec:
171
+ imagePullSecrets:
172
+ - name: ${profile.secrets.imagePull}
173
+ securityContext:
174
+ fsGroup: 1000
175
+ seccompProfile: { type: RuntimeDefault }
176
+ containers:
177
+ - name: redis
178
+ image: ${quote(image ?? dependency.image)}
179
+ imagePullPolicy: IfNotPresent
180
+ command: [sh, -c, 'exec redis-server --appendonly yes --dir /data --requirepass "$REDIS_PASSWORD"']
181
+ env:
182
+ - name: REDIS_PASSWORD
183
+ valueFrom:
184
+ secretKeyRef: { name: ${dependency.secretName}, key: REDIS_PASSWORD }
185
+ ports:
186
+ - { name: redis, containerPort: 6379 }
187
+ readinessProbe:
188
+ exec:
189
+ command: [sh, -c, 'redis-cli -a "$REDIS_PASSWORD" --no-auth-warning ping | grep -qx PONG']
190
+ periodSeconds: 5
191
+ livenessProbe:
192
+ exec:
193
+ command: [sh, -c, 'redis-cli -a "$REDIS_PASSWORD" --no-auth-warning ping | grep -qx PONG']
194
+ initialDelaySeconds: 20
195
+ periodSeconds: 10
196
+ resources:
197
+ requests: { cpu: "50m", memory: "64Mi" }
198
+ limits: { cpu: "500m", memory: "256Mi" }
199
+ volumeMounts:
200
+ - { name: data, mountPath: /data }
201
+ volumeClaimTemplates:
202
+ - metadata:
203
+ name: data
204
+ spec:
205
+ ${storageClassLine(dependency)} accessModes: [ReadWriteOnce]
206
+ resources:
207
+ requests:
208
+ storage: ${quote(dependency.storageSize)}
209
+ `;
210
+ }
211
+ function objectStorageResources(profile, image, clientImage, rolloutStateDigest) {
212
+ const dependency = profile.dependencies.objectStorage;
213
+ if (dependency.mode !== "inCluster")
214
+ return "";
215
+ const name = `${profile.target.release}-minio`;
216
+ const initializationHash = (0, node_crypto_1.createHash)("sha256")
217
+ .update(`${clientImage ?? dependency.clientImage}\0${dependency.bucket}`)
218
+ .digest("hex")
219
+ .slice(0, 8);
220
+ return `apiVersion: v1
221
+ kind: Service
222
+ metadata:
223
+ name: ${name}
224
+ spec:
225
+ selector:
226
+ app.kubernetes.io/name: ${name}
227
+ ports:
228
+ - { name: api, port: 9000, targetPort: 9000 }
229
+ ---
230
+ apiVersion: apps/v1
231
+ kind: StatefulSet
232
+ metadata:
233
+ name: ${name}
234
+ spec:
235
+ serviceName: ${name}
236
+ replicas: 1
237
+ selector:
238
+ matchLabels:
239
+ app.kubernetes.io/name: ${name}
240
+ template:
241
+ metadata:
242
+ ${podTemplateAnnotations(rolloutStateDigest)}
243
+ labels:
244
+ ${labels(name, 8)} spec:
245
+ imagePullSecrets:
246
+ - name: ${profile.secrets.imagePull}
247
+ securityContext:
248
+ fsGroup: 1000
249
+ seccompProfile: { type: RuntimeDefault }
250
+ containers:
251
+ - name: minio
252
+ image: ${quote(image ?? dependency.image)}
253
+ imagePullPolicy: IfNotPresent
254
+ args: [server, /data]
255
+ env:
256
+ - name: MINIO_ROOT_USER
257
+ valueFrom:
258
+ secretKeyRef: { name: ${dependency.secretName}, key: MINIO_ROOT_USER }
259
+ - name: MINIO_ROOT_PASSWORD
260
+ valueFrom:
261
+ secretKeyRef: { name: ${dependency.secretName}, key: MINIO_ROOT_PASSWORD }
262
+ ports:
263
+ - { name: api, containerPort: 9000 }
264
+ readinessProbe:
265
+ httpGet: { path: /minio/health/ready, port: 9000 }
266
+ periodSeconds: 5
267
+ livenessProbe:
268
+ httpGet: { path: /minio/health/live, port: 9000 }
269
+ initialDelaySeconds: 20
270
+ periodSeconds: 10
271
+ resources:
272
+ requests: { cpu: "250m", memory: "512Mi" }
273
+ limits: { cpu: "2000m", memory: "2Gi" }
274
+ volumeMounts:
275
+ - { name: data, mountPath: /data }
276
+ volumeClaimTemplates:
277
+ - metadata:
278
+ name: data
279
+ spec:
280
+ ${storageClassLine(dependency)} accessModes: [ReadWriteOnce]
281
+ resources:
282
+ requests:
283
+ storage: ${quote(dependency.storageSize)}
284
+ ---
285
+ apiVersion: batch/v1
286
+ kind: Job
287
+ metadata:
288
+ name: ${profile.target.release}-minio-init-${initializationHash}
289
+ spec:
290
+ backoffLimit: 6
291
+ ttlSecondsAfterFinished: 600
292
+ template:
293
+ metadata:
294
+ labels:
295
+ ${labels(`${profile.target.release}-minio-initialize`, 8)} spec:
296
+ imagePullSecrets:
297
+ - name: ${profile.secrets.imagePull}
298
+ restartPolicy: OnFailure
299
+ securityContext:
300
+ seccompProfile: { type: RuntimeDefault }
301
+ containers:
302
+ - name: initialize
303
+ image: ${quote(clientImage ?? dependency.clientImage)}
304
+ imagePullPolicy: IfNotPresent
305
+ command:
306
+ - sh
307
+ - -ec
308
+ - |
309
+ until mc alias set target http://${name}:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do sleep 2; done
310
+ mc mb --ignore-existing target/${dependency.bucket}
311
+ cat >/tmp/policy.json <<'POLICY'
312
+ {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetBucketLocation","s3:ListBucket"],"Resource":["arn:aws:s3:::${dependency.bucket}"]},{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject","s3:DeleteObject"],"Resource":["arn:aws:s3:::${dependency.bucket}/*"]}]}
313
+ POLICY
314
+ mc admin policy create target podokit-app /tmp/policy.json || mc admin policy info target podokit-app
315
+ mc admin user add target "$S3_ACCESS_KEY_ID" "$S3_SECRET_ACCESS_KEY" || true
316
+ mc admin policy attach target podokit-app --user "$S3_ACCESS_KEY_ID"
317
+ env:
318
+ - name: MINIO_ROOT_USER
319
+ valueFrom:
320
+ secretKeyRef: { name: ${dependency.secretName}, key: MINIO_ROOT_USER }
321
+ - name: MINIO_ROOT_PASSWORD
322
+ valueFrom:
323
+ secretKeyRef: { name: ${dependency.secretName}, key: MINIO_ROOT_PASSWORD }
324
+ - name: S3_ACCESS_KEY_ID
325
+ valueFrom:
326
+ secretKeyRef: { name: ${dependency.secretName}, key: S3_ACCESS_KEY_ID }
327
+ - name: S3_SECRET_ACCESS_KEY
328
+ valueFrom:
329
+ secretKeyRef: { name: ${dependency.secretName}, key: S3_SECRET_ACCESS_KEY }
330
+ `;
331
+ }
332
+ function dependencyManifest(profile, images, rolloutStateDigest) {
333
+ return [
334
+ postgresResources(profile, images.postgres, rolloutStateDigest),
335
+ redisResources(profile, images.redis, rolloutStateDigest),
336
+ objectStorageResources(profile, images.objectStorage, images.objectStorageClient, rolloutStateDigest),
337
+ ]
338
+ .filter(Boolean)
339
+ .join("\n---\n");
340
+ }
341
+ function derivedRuntimeConfig(profile) {
342
+ const result = {
343
+ PORT: "3000",
344
+ BACKEND_INTERNAL_URL: `http://${profile.target.release}-api:3000`,
345
+ ...profile.runtimeConfig,
346
+ };
347
+ if (profile.dependencies.postgres.mode === "inCluster") {
348
+ result.POSTGRES_HOST = `${profile.target.release}-postgres`;
349
+ result.POSTGRES_PORT = "5432";
350
+ }
351
+ if (profile.dependencies.redis.mode === "inCluster") {
352
+ result.REDIS_HOST = `${profile.target.release}-redis`;
353
+ result.REDIS_PORT = "6379";
354
+ }
355
+ if (profile.dependencies.objectStorage.mode === "inCluster") {
356
+ result.S3_ENDPOINT = `http://${profile.target.release}-minio:9000`;
357
+ result.S3_BUCKET = profile.dependencies.objectStorage.bucket;
358
+ result.S3_FORCE_PATH_STYLE = "true";
359
+ }
360
+ return result;
361
+ }
362
+ function derivedRuntimeConfigDigest(profile) {
363
+ const entries = Object.entries(derivedRuntimeConfig(profile)).sort(([left], [right]) => left.localeCompare(right));
364
+ return digest(["runtime-config", JSON.stringify(entries)]);
365
+ }
366
+ function configMap(profile) {
367
+ const entries = Object.entries(derivedRuntimeConfig(profile)).sort(([left], [right]) => left.localeCompare(right));
368
+ return `apiVersion: v1
369
+ kind: ConfigMap
370
+ metadata:
371
+ name: ${profile.target.release}-runtime
372
+ ${entries.length ? `data:\n${entries.map(([key, value]) => ` ${key}: ${quote(value)}`).join("\n")}` : "data: {}"}
373
+ `;
374
+ }
375
+ function deployment(profile, kind, workload, image, rolloutStateDigest, runtimeConfigDigest) {
376
+ const name = `${profile.target.release}-${kind}`;
377
+ const workloadSecret = kind === "api" ? profile.secrets.api : profile.secrets.web;
378
+ const secretEnvFrom = workloadSecret
379
+ ? ` - secretRef: { name: ${workloadSecret.name} }
380
+ `
381
+ : "";
382
+ const probe = kind === "api"
383
+ ? ` readinessProbe:
384
+ httpGet: { path: /health/ready, port: 3000 }
385
+ periodSeconds: 5
386
+ livenessProbe:
387
+ httpGet: { path: /health, port: 3000 }
388
+ initialDelaySeconds: 20
389
+ periodSeconds: 10
390
+ startupProbe:
391
+ httpGet: { path: /health, port: 3000 }
392
+ failureThreshold: 30
393
+ periodSeconds: 2
394
+ `
395
+ : ` readinessProbe:
396
+ httpGet: { path: /, port: 3000 }
397
+ periodSeconds: 5
398
+ livenessProbe:
399
+ httpGet: { path: /, port: 3000 }
400
+ initialDelaySeconds: 20
401
+ periodSeconds: 10
402
+ `;
403
+ return `apiVersion: apps/v1
404
+ kind: Deployment
405
+ metadata:
406
+ name: ${name}
407
+ labels:
408
+ ${labels(name)}spec:
409
+ replicas: ${workload.replicas}
410
+ strategy:
411
+ type: RollingUpdate
412
+ rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
413
+ selector:
414
+ matchLabels:
415
+ app.kubernetes.io/name: ${name}
416
+ template:
417
+ metadata:
418
+ ${podTemplateAnnotations(rolloutStateDigest, runtimeConfigDigest)}
419
+ labels:
420
+ ${labels(name, 8)} spec:
421
+ imagePullSecrets:
422
+ - name: ${profile.secrets.imagePull}
423
+ securityContext:
424
+ runAsNonRoot: true
425
+ runAsUser: 100
426
+ runAsGroup: 101
427
+ seccompProfile: { type: RuntimeDefault }
428
+ containers:
429
+ - name: ${kind}
430
+ image: ${quote(image)}
431
+ imagePullPolicy: IfNotPresent
432
+ securityContext:
433
+ allowPrivilegeEscalation: false
434
+ capabilities: { drop: [ALL] }
435
+ envFrom:
436
+ - configMapRef: { name: ${profile.target.release}-runtime }
437
+ ${secretEnvFrom} ports:
438
+ - { name: http, containerPort: 3000 }
439
+ ${probe}${resources(workload.resources)}---
440
+ apiVersion: v1
441
+ kind: Service
442
+ metadata:
443
+ name: ${name}
444
+ spec:
445
+ selector:
446
+ app.kubernetes.io/name: ${name}
447
+ ports:
448
+ - { name: http, port: 3000, targetPort: 3000 }
449
+ `;
450
+ }
451
+ function workerDeployment(profile, image, rolloutStateDigest, runtimeConfigDigest) {
452
+ const workload = profile.workloads.worker;
453
+ if (!workload)
454
+ return "";
455
+ const name = `${profile.target.release}-worker`;
456
+ return `apiVersion: apps/v1
457
+ kind: Deployment
458
+ metadata:
459
+ name: ${name}
460
+ labels:
461
+ ${labels(name)}spec:
462
+ replicas: ${workload.replicas}
463
+ strategy:
464
+ type: RollingUpdate
465
+ rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
466
+ selector:
467
+ matchLabels:
468
+ app.kubernetes.io/name: ${name}
469
+ template:
470
+ metadata:
471
+ ${podTemplateAnnotations(rolloutStateDigest, runtimeConfigDigest)}
472
+ labels:
473
+ ${labels(name, 8)} spec:
474
+ imagePullSecrets:
475
+ - name: ${profile.secrets.imagePull}
476
+ securityContext:
477
+ runAsNonRoot: true
478
+ runAsUser: 100
479
+ runAsGroup: 101
480
+ seccompProfile: { type: RuntimeDefault }
481
+ containers:
482
+ - name: worker
483
+ image: ${quote(image)}
484
+ imagePullPolicy: IfNotPresent
485
+ command: [node, dist/main-worker]
486
+ envFrom:
487
+ - configMapRef: { name: ${profile.target.release}-runtime }
488
+ - secretRef: { name: ${profile.secrets.api.name} }
489
+ securityContext:
490
+ allowPrivilegeEscalation: false
491
+ capabilities: { drop: [ALL] }
492
+ ${resources(workload.resources)}`;
493
+ }
494
+ function disruptionBudget(profile, kind) {
495
+ const workload = profile.workloads[kind];
496
+ if (!workload || workload.replicas < 2)
497
+ return "";
498
+ const name = `${profile.target.release}-${kind}`;
499
+ return `apiVersion: policy/v1
500
+ kind: PodDisruptionBudget
501
+ metadata:
502
+ name: ${name}
503
+ spec:
504
+ minAvailable: 1
505
+ selector:
506
+ matchLabels:
507
+ app.kubernetes.io/name: ${name}
508
+ `;
509
+ }
510
+ function exposure(profile) {
511
+ const service = `${profile.target.release}-web`;
512
+ if (profile.exposure.mode === "nodePort") {
513
+ return `apiVersion: v1
514
+ kind: Service
515
+ metadata:
516
+ name: ${service}-public
517
+ spec:
518
+ type: NodePort
519
+ selector:
520
+ app.kubernetes.io/name: ${service}
521
+ ports:
522
+ - { name: http, port: 80, targetPort: 3000 }
523
+ `;
524
+ }
525
+ const tls = profile.exposure.tlsSecretName
526
+ ? ` tls:
527
+ - hosts: [${quote(profile.exposure.host)}]
528
+ secretName: ${quote(profile.exposure.tlsSecretName)}
529
+ `
530
+ : "";
531
+ return `apiVersion: networking.k8s.io/v1
532
+ kind: Ingress
533
+ metadata:
534
+ name: ${profile.target.release}
535
+ spec:
536
+ ingressClassName: ${quote(profile.exposure.ingressClassName)}
537
+ ${tls} rules:
538
+ - host: ${quote(profile.exposure.host)}
539
+ http:
540
+ paths:
541
+ - path: /
542
+ pathType: Prefix
543
+ backend:
544
+ service:
545
+ name: ${service}
546
+ port: { number: 3000 }
547
+ `;
548
+ }
549
+ function applicationManifest(profile, images, rolloutStateDigest) {
550
+ const runtimeConfigDigest = derivedRuntimeConfigDigest(profile);
551
+ return [
552
+ configMap(profile),
553
+ deployment(profile, "api", profile.workloads.api, images.api, rolloutStateDigest, runtimeConfigDigest),
554
+ deployment(profile, "web", profile.workloads.web, images.web, rolloutStateDigest, runtimeConfigDigest),
555
+ workerDeployment(profile, images.api, rolloutStateDigest, runtimeConfigDigest),
556
+ disruptionBudget(profile, "api"),
557
+ disruptionBudget(profile, "web"),
558
+ profile.workloads.worker ? disruptionBudget(profile, "worker") : "",
559
+ exposure(profile),
560
+ ]
561
+ .filter(Boolean)
562
+ .join("\n---\n");
563
+ }
564
+ function migrationRuntimeEnv(profile) {
565
+ return Object.entries(derivedRuntimeConfig(profile))
566
+ .sort(([left], [right]) => left.localeCompare(right))
567
+ .map(([key, value]) => ` - name: ${key}
568
+ value: ${quote(value)}`)
569
+ .join("\n");
570
+ }
571
+ function migrationManifest(profile, release, images) {
572
+ const releaseSlug = release.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
573
+ return `apiVersion: batch/v1
574
+ kind: Job
575
+ metadata:
576
+ name: ${profile.target.release}-migrate-${releaseSlug}
577
+ namespace: ${profile.target.namespace}
578
+ labels:
579
+ ${labels(`${profile.target.release}-migration`)}spec:
580
+ backoffLimit: 1
581
+ ttlSecondsAfterFinished: 3600
582
+ template:
583
+ metadata:
584
+ labels:
585
+ ${labels(`${profile.target.release}-migration`, 8)} spec:
586
+ restartPolicy: Never
587
+ imagePullSecrets:
588
+ - name: ${profile.secrets.imagePull}
589
+ securityContext:
590
+ runAsNonRoot: true
591
+ runAsUser: 100
592
+ runAsGroup: 101
593
+ seccompProfile: { type: RuntimeDefault }
594
+ containers:
595
+ - name: migrate
596
+ image: ${quote(images.api)}
597
+ imagePullPolicy: IfNotPresent
598
+ command: [npm, run, migrate:all]
599
+ env:
600
+ ${migrationRuntimeEnv(profile)}
601
+ envFrom:
602
+ - secretRef: { name: ${profile.secrets.api.name} }
603
+ securityContext:
604
+ allowPrivilegeEscalation: false
605
+ capabilities: { drop: [ALL] }
606
+ `;
607
+ }
608
+ function chart(directory, name, manifest) {
609
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(directory, "templates"), { recursive: true });
610
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, "Chart.yaml"), `apiVersion: v2\nname: ${name}\ndescription: Generated by PodoKit\nversion: 0.1.0\ntype: application\n`);
611
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, "resources.yaml"), manifest || "# No resources\n");
612
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, "templates", "resources.yaml"), '{{ .Files.Get "resources.yaml" }}\n');
613
+ }
614
+ function renderDeployment(projectRoot, profileName, profile, release, resolvedImages, rolloutStateDigest) {
615
+ (0, deploy_profile_1.profilePath)(projectRoot, profileName);
616
+ const images = resolvedImages ?? {
617
+ api: `${profile.release.apiRepository}:${release}`,
618
+ web: `${profile.release.webRepository}:${release}`,
619
+ postgres: profile.dependencies.postgres.mode === "inCluster"
620
+ ? profile.dependencies.postgres.image
621
+ : null,
622
+ redis: profile.dependencies.redis.mode === "inCluster"
623
+ ? profile.dependencies.redis.image
624
+ : null,
625
+ objectStorage: profile.dependencies.objectStorage.mode === "inCluster"
626
+ ? profile.dependencies.objectStorage.image
627
+ : null,
628
+ objectStorageClient: profile.dependencies.objectStorage.mode === "inCluster"
629
+ ? profile.dependencies.objectStorage.clientImage
630
+ : null,
631
+ };
632
+ const root = (0, node_path_1.join)((0, node_path_1.resolve)(projectRoot), ".podokit", "runtime", "deploy", profileName);
633
+ (0, node_fs_1.rmSync)(root, { recursive: true, force: true });
634
+ const dependencyChart = (0, node_path_1.join)(root, "dependencies");
635
+ const applicationChart = (0, node_path_1.join)(root, "application");
636
+ const effectiveRolloutStateDigest = rolloutStateDigest ?? offlineRolloutStateDigest(profile);
637
+ const dependencies = dependencyManifest(profile, images, effectiveRolloutStateDigest);
638
+ const application = applicationManifest(profile, images, effectiveRolloutStateDigest);
639
+ const migration = migrationManifest(profile, release, images);
640
+ chart(dependencyChart, "podokit-dependencies", dependencies);
641
+ chart(applicationChart, "podokit-application", application);
642
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, "migration.yaml"), migration);
643
+ return {
644
+ root,
645
+ dependencyChart,
646
+ applicationChart,
647
+ migrationManifest: (0, node_path_1.join)(root, "migration.yaml"),
648
+ dependencyManifest: dependencies,
649
+ applicationManifest: application,
650
+ };
651
+ }