@vibesdotdev/infra-doks 0.0.1

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 (113) hide show
  1. package/README.md +107 -0
  2. package/SPEC.md +285 -0
  3. package/dist/client/digitalocean-app-deploy.client.d.ts +46 -0
  4. package/dist/client/digitalocean-app-deploy.client.d.ts.map +1 -0
  5. package/dist/client/digitalocean-app-deploy.client.js +135 -0
  6. package/dist/client/digitalocean-app-deploy.client.js.map +1 -0
  7. package/dist/client/index.d.ts +15 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +18 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/cloud/base.d.ts +33 -0
  12. package/dist/cloud/base.d.ts.map +1 -0
  13. package/dist/cloud/base.js +86 -0
  14. package/dist/cloud/base.js.map +1 -0
  15. package/dist/cloud/digitalocean.d.ts +33 -0
  16. package/dist/cloud/digitalocean.d.ts.map +1 -0
  17. package/dist/cloud/digitalocean.js +258 -0
  18. package/dist/cloud/digitalocean.js.map +1 -0
  19. package/dist/cloud/factory.d.ts +28 -0
  20. package/dist/cloud/factory.d.ts.map +1 -0
  21. package/dist/cloud/factory.js +151 -0
  22. package/dist/cloud/factory.js.map +1 -0
  23. package/dist/cloud/index.d.ts +12 -0
  24. package/dist/cloud/index.d.ts.map +1 -0
  25. package/dist/cloud/index.js +11 -0
  26. package/dist/cloud/index.js.map +1 -0
  27. package/dist/doks.plugin.d.ts +41 -0
  28. package/dist/doks.plugin.d.ts.map +1 -0
  29. package/dist/doks.plugin.js +287 -0
  30. package/dist/doks.plugin.js.map +1 -0
  31. package/dist/implementations/deployment.impl.d.ts +34 -0
  32. package/dist/implementations/deployment.impl.d.ts.map +1 -0
  33. package/dist/implementations/deployment.impl.js +86 -0
  34. package/dist/implementations/deployment.impl.js.map +1 -0
  35. package/dist/implementations/droplet.impl.d.ts +85 -0
  36. package/dist/implementations/droplet.impl.d.ts.map +1 -0
  37. package/dist/implementations/droplet.impl.js +113 -0
  38. package/dist/implementations/droplet.impl.js.map +1 -0
  39. package/dist/implementations/gitea.impl.d.ts +68 -0
  40. package/dist/implementations/gitea.impl.d.ts.map +1 -0
  41. package/dist/implementations/gitea.impl.js +295 -0
  42. package/dist/implementations/gitea.impl.js.map +1 -0
  43. package/dist/implementations/managed-db.impl.d.ts +25 -0
  44. package/dist/implementations/managed-db.impl.d.ts.map +1 -0
  45. package/dist/implementations/managed-db.impl.js +31 -0
  46. package/dist/implementations/managed-db.impl.js.map +1 -0
  47. package/dist/implementations/managed-redis.impl.d.ts +37 -0
  48. package/dist/implementations/managed-redis.impl.d.ts.map +1 -0
  49. package/dist/implementations/managed-redis.impl.js +40 -0
  50. package/dist/implementations/managed-redis.impl.js.map +1 -0
  51. package/dist/implementations/spaces.impl.d.ts +36 -0
  52. package/dist/implementations/spaces.impl.d.ts.map +1 -0
  53. package/dist/implementations/spaces.impl.js +40 -0
  54. package/dist/implementations/spaces.impl.js.map +1 -0
  55. package/dist/implementations/statefulset.impl.d.ts +65 -0
  56. package/dist/implementations/statefulset.impl.d.ts.map +1 -0
  57. package/dist/implementations/statefulset.impl.js +165 -0
  58. package/dist/implementations/statefulset.impl.js.map +1 -0
  59. package/dist/implementations/verdaccio.impl.d.ts +65 -0
  60. package/dist/implementations/verdaccio.impl.d.ts.map +1 -0
  61. package/dist/implementations/verdaccio.impl.js +259 -0
  62. package/dist/implementations/verdaccio.impl.js.map +1 -0
  63. package/dist/index.d.ts +15 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +19 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/kubernetes/index.d.ts +95 -0
  68. package/dist/kubernetes/index.d.ts.map +1 -0
  69. package/dist/kubernetes/index.js +625 -0
  70. package/dist/kubernetes/index.js.map +1 -0
  71. package/dist/secrets/index.d.ts +4 -0
  72. package/dist/secrets/index.d.ts.map +1 -0
  73. package/dist/secrets/index.js +4 -0
  74. package/dist/secrets/index.js.map +1 -0
  75. package/dist/secrets/vault.descriptor.d.ts +10 -0
  76. package/dist/secrets/vault.descriptor.d.ts.map +1 -0
  77. package/dist/secrets/vault.descriptor.js +25 -0
  78. package/dist/secrets/vault.descriptor.js.map +1 -0
  79. package/dist/secrets/vault.impl.cloud.d.ts +40 -0
  80. package/dist/secrets/vault.impl.cloud.d.ts.map +1 -0
  81. package/dist/secrets/vault.impl.cloud.js +178 -0
  82. package/dist/secrets/vault.impl.cloud.js.map +1 -0
  83. package/dist/secrets/vault.impl.d.ts +29 -0
  84. package/dist/secrets/vault.impl.d.ts.map +1 -0
  85. package/dist/secrets/vault.impl.js +137 -0
  86. package/dist/secrets/vault.impl.js.map +1 -0
  87. package/dist/types.d.ts +509 -0
  88. package/dist/types.d.ts.map +1 -0
  89. package/dist/types.js +47 -0
  90. package/dist/types.js.map +1 -0
  91. package/package.json +145 -0
  92. package/src/client/digitalocean-app-deploy.client.ts +226 -0
  93. package/src/client/index.ts +24 -0
  94. package/src/cloud/base.ts +149 -0
  95. package/src/cloud/digitalocean.ts +363 -0
  96. package/src/cloud/factory.ts +190 -0
  97. package/src/cloud/index.ts +81 -0
  98. package/src/doks.plugin.ts +401 -0
  99. package/src/implementations/deployment.impl.ts +93 -0
  100. package/src/implementations/droplet.impl.ts +157 -0
  101. package/src/implementations/gitea.impl.ts +319 -0
  102. package/src/implementations/managed-db.impl.ts +37 -0
  103. package/src/implementations/managed-redis.impl.ts +49 -0
  104. package/src/implementations/spaces.impl.ts +52 -0
  105. package/src/implementations/statefulset.impl.ts +186 -0
  106. package/src/implementations/verdaccio.impl.ts +300 -0
  107. package/src/index.ts +136 -0
  108. package/src/kubernetes/index.ts +754 -0
  109. package/src/secrets/index.ts +9 -0
  110. package/src/secrets/vault.descriptor.ts +28 -0
  111. package/src/secrets/vault.impl.cloud.ts +278 -0
  112. package/src/secrets/vault.impl.ts +149 -0
  113. package/src/types.ts +563 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Implementation: infra/git-hosting -> gitea-doks
3
+ *
4
+ * Generates K8s manifests for a Gitea git hosting service on DOKS.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ import * as z from 'zod/v4';
8
+ import { createRuntimeImplementation } from '@vibesdotdev/runtime';
9
+ import { GiteaDoksGitHostingConfigSchema, GitHostingTypeSchema } from '@vibesdotdev/infra-core/kinds';
10
+ import { loadGiteaThemeAssets } from '@vibesdotdev/registry-theme';
11
+ import type { K8sDeployment, K8sService } from '../types.ts';
12
+
13
+ export const GiteaDoksDescriptorSchema = z.object({
14
+ kind: z.literal('infra/git-hosting'),
15
+ id: z.string().min(1),
16
+ name: z.string().optional(),
17
+ description: z.string().optional(),
18
+ url: z.string().url(),
19
+ hostname: z.string().min(1),
20
+ type: GitHostingTypeSchema,
21
+ storagePath: z.string().min(1).default('/data'),
22
+ authMethod: z.enum(['local', 'oauth', 'token']).default('token'),
23
+ ssl: z.boolean().default(true),
24
+ env: z.array(z.object({
25
+ name: z.string().regex(/^[_A-Z0-9]+$/),
26
+ public: z.boolean().default(false),
27
+ required: z.boolean().default(true),
28
+ value: z.string().optional(),
29
+ secret: z.boolean().default(false),
30
+ description: z.string().optional(),
31
+ storeKey: z.string().min(1).optional()
32
+ })).optional(),
33
+ config: GiteaDoksGitHostingConfigSchema.extend({
34
+ adapter: z.literal('gitea-doks')
35
+ })
36
+ });
37
+
38
+ export type GiteaDoksDescriptorInput = z.input<typeof GiteaDoksDescriptorSchema>;
39
+ export type GiteaDoksDescriptor = z.infer<typeof GiteaDoksDescriptorSchema>;
40
+
41
+ export interface GiteaManifest {
42
+ deployment: K8sDeployment;
43
+ service: K8sService;
44
+ pvc: { apiVersion: string; kind: string; metadata: Record<string, unknown>; spec: Record<string, unknown> };
45
+ themeConfigMap: { apiVersion: string; kind: string; metadata: Record<string, unknown>; data: Record<string, string> };
46
+ ingress: { apiVersion: string; kind: string; metadata: Record<string, unknown>; spec: Record<string, unknown> };
47
+ }
48
+
49
+ /**
50
+ * ConfigMap keys → relative paths under the staging mount. The install-theme
51
+ * initContainer copies these into the PVC's gitea/custom/ tree where Gitea
52
+ * picks them up at runtime. Keys are dot-separated because K8s ConfigMap keys
53
+ * cannot contain slashes; the volume `items[].path` translation produces the
54
+ * final on-disk layout.
55
+ */
56
+ const THEME_FILES: Readonly<Record<string, string>> = {
57
+ 'public.css.vibes-tokens.css': 'public/css/vibes-tokens.css',
58
+ 'public.css.vibes-overlay.css': 'public/css/vibes-overlay.css',
59
+ 'public.img.logo.svg': 'public/img/logo.svg',
60
+ 'public.img.favicon.svg': 'public/img/favicon.svg',
61
+ 'templates.custom.header.tmpl': 'templates/custom/header.tmpl',
62
+ 'templates.custom.body_outer_pre.tmpl': 'templates/custom/body_outer_pre.tmpl',
63
+ 'templates.custom.extra_links_footer.tmpl': 'templates/custom/extra_links_footer.tmpl',
64
+ // home.tmpl REPLACES the stock home page (not under custom/) — go-template
65
+ // resolution checks templates/home.tmpl first, then falls back to embedded.
66
+ 'templates.home.tmpl': 'templates/home.tmpl',
67
+ // head_navbar.tmpl REPLACES the stock <nav id="navbar">. We render the
68
+ // vibes-brand-bar inline here instead of duplicating chrome via body_outer_pre.
69
+ 'templates.base.head_navbar.tmpl': 'templates/base/head_navbar.tmpl'
70
+ };
71
+
72
+ const ELEMENTS_KEY = 'public.js.elements.js';
73
+ const ELEMENTS_PATH = 'public/js/elements.js';
74
+
75
+ export function generateGiteaManifest(input: GiteaDoksDescriptorInput): GiteaManifest {
76
+ const d = GiteaDoksDescriptorSchema.parse(input);
77
+ const ns = 'vibes';
78
+ const name = `vibes-${d.id}`;
79
+ const pvcName = `${name}-data`;
80
+ const themeCmName = `${name}-theme`;
81
+ const themeStagingPath = '/etc/gitea-theme';
82
+ // The official gitea/gitea image sets GITEA_CUSTOM=${GITEA_WORK_DIR}, where
83
+ // GITEA_WORK_DIR defaults to /data/gitea (NOT /data/gitea/custom). So
84
+ // custom templates and static assets live directly under /data/gitea/.
85
+ const giteaCustomPath = `${d.storagePath}/gitea`;
86
+ const cfg = d.config!;
87
+ const port = cfg.httpPort;
88
+ const secretName = 'gitea-secrets';
89
+
90
+ const themeAssets = loadGiteaThemeAssets();
91
+ const themeData: Record<string, string> = {
92
+ 'public.css.vibes-tokens.css': themeAssets.tokensCss,
93
+ 'public.css.vibes-overlay.css': themeAssets.overlayCss,
94
+ 'public.img.logo.svg': themeAssets.logoSvg,
95
+ 'public.img.favicon.svg': themeAssets.faviconSvg,
96
+ 'templates.custom.header.tmpl': themeAssets.headerTmpl,
97
+ 'templates.custom.body_outer_pre.tmpl': themeAssets.bodyOuterPreTmpl,
98
+ 'templates.custom.extra_links_footer.tmpl': themeAssets.extraLinksFooterTmpl,
99
+ 'templates.home.tmpl': themeAssets.homeTmpl,
100
+ 'templates.base.head_navbar.tmpl': themeAssets.headNavbarTmpl
101
+ };
102
+ const themeItems = Object.entries(THEME_FILES).map(([key, path]) => ({ key, path }));
103
+ if (themeAssets.elementsBundle) {
104
+ themeData[ELEMENTS_KEY] = themeAssets.elementsBundle;
105
+ themeItems.push({ key: ELEMENTS_KEY, path: ELEMENTS_PATH });
106
+ }
107
+ // Hash of theme content so any change causes a rollout (otherwise a
108
+ // ConfigMap-only diff leaves pods with stale assets in their PVC).
109
+ const themeChecksum = createHash('sha256')
110
+ .update(Object.entries(themeData).sort().map(([k, v]) => `${k}:${v}`).join('\n'))
111
+ .digest('hex')
112
+ .slice(0, 16);
113
+
114
+ // NOTE: the previous Gitea bootstrap container has been removed. It tried to
115
+ // curl localhost:3000 from inside an *init* container — but Gitea hadn't
116
+ // started yet (init containers run first), so it could never have worked.
117
+ // Admin user creation and OAuth provider registration should run as a
118
+ // separate Job (or via `gitea admin user create` exec) post-Ready.
119
+
120
+ const deployment: K8sDeployment = {
121
+ apiVersion: 'apps/v1',
122
+ kind: 'Deployment',
123
+ metadata: {
124
+ name,
125
+ namespace: ns,
126
+ labels: { app: d.id, 'app.kubernetes.io/part-of': 'vibes' },
127
+ annotations: {
128
+ 'secrets.hashicorp.com/rollout-restart': 'true'
129
+ }
130
+ },
131
+ spec: {
132
+ replicas: 1,
133
+ // PVC is ReadWriteOnce — RollingUpdate would Multi-Attach-deadlock.
134
+ // Recreate stops the old pod, then starts the new one with the freed volume.
135
+ strategy: { type: 'Recreate' },
136
+ selector: { matchLabels: { app: d.id } },
137
+ template: {
138
+ metadata: {
139
+ labels: { app: d.id },
140
+ annotations: { 'vibes.dev/theme-checksum': themeChecksum }
141
+ },
142
+ spec: {
143
+ initContainers: [
144
+ {
145
+ name: 'chown',
146
+ image: 'busybox:1.36',
147
+ command: ['sh', '-c', `chown -R 1000:1000 ${d.storagePath}`],
148
+ volumeMounts: [{ name: 'data', mountPath: d.storagePath }],
149
+ resources: { requests: { cpu: '50m', memory: '32Mi' }, limits: { memory: '128Mi' } }
150
+ },
151
+ {
152
+ name: 'install-theme',
153
+ image: 'busybox:1.36',
154
+ command: ['sh', '-c', [
155
+ // Gitea v1.21 expects custom static assets under `public/assets/<type>/`
156
+ // (NOT `public/<type>/` — that's a legacy layout it now rejects).
157
+ `mkdir -p ${giteaCustomPath}/public/assets/css`,
158
+ `mkdir -p ${giteaCustomPath}/public/assets/img`,
159
+ `mkdir -p ${giteaCustomPath}/public/assets/js`,
160
+ `mkdir -p ${giteaCustomPath}/templates/custom`,
161
+ // Versioned filenames (.v5.) sidestep Cloudflare negative-cache on the unversioned URLs.
162
+ // Bump the version when bytes change to invalidate browser/edge caches.
163
+ `cp -f ${themeStagingPath}/public/css/vibes-tokens.css ${giteaCustomPath}/public/assets/css/vibes-tokens.v5.css`,
164
+ `cp -f ${themeStagingPath}/public/css/vibes-overlay.css ${giteaCustomPath}/public/assets/css/vibes-overlay.v5.css`,
165
+ `cp -f ${themeStagingPath}/public/img/logo.svg ${giteaCustomPath}/public/assets/img/logo.v5.svg`,
166
+ `cp -f ${themeStagingPath}/public/img/favicon.svg ${giteaCustomPath}/public/assets/img/favicon.v5.svg`,
167
+ `cp -f ${themeStagingPath}/templates/custom/header.tmpl ${giteaCustomPath}/templates/custom/header.tmpl`,
168
+ `cp -f ${themeStagingPath}/templates/custom/body_outer_pre.tmpl ${giteaCustomPath}/templates/custom/body_outer_pre.tmpl`,
169
+ `cp -f ${themeStagingPath}/templates/custom/extra_links_footer.tmpl ${giteaCustomPath}/templates/custom/extra_links_footer.tmpl`,
170
+ `cp -f ${themeStagingPath}/templates/home.tmpl ${giteaCustomPath}/templates/home.tmpl`,
171
+ `mkdir -p ${giteaCustomPath}/templates/base`,
172
+ `cp -f ${themeStagingPath}/templates/base/head_navbar.tmpl ${giteaCustomPath}/templates/base/head_navbar.tmpl`,
173
+ // elements.js is optional — copy only if the staging volume includes it (i.e. the @vibesdotdev/elements bundle was built before manifest generation).
174
+ `if [ -f ${themeStagingPath}/public/js/elements.js ]; then cp -f ${themeStagingPath}/public/js/elements.js ${giteaCustomPath}/public/assets/js/elements.v5.js; fi`,
175
+ // Clean up files from the previous wrong layouts so they don't confuse Gitea's legacy detection.
176
+ `rm -rf ${giteaCustomPath}/public/css ${giteaCustomPath}/public/img ${giteaCustomPath}/public/js ${giteaCustomPath}/custom 2>/dev/null || true`,
177
+ // Update APP_NAME in app.ini directly. Gitea v1.21 doesn't have
178
+ // `environment-to-ini`, so GITEA__DEFAULT__APP_NAME never reaches
179
+ // the running config. sed is idempotent: if the line already
180
+ // matches we no-op.
181
+ `if [ -f ${giteaCustomPath}/conf/app.ini ]; then sed -i 's|^APP_NAME = .*$|APP_NAME = Vibes Git|' ${giteaCustomPath}/conf/app.ini || true; fi`,
182
+ `chown -R 1000:1000 ${giteaCustomPath}`,
183
+ 'echo "vibes theme installed"'
184
+ ].join(' && ')],
185
+ volumeMounts: [
186
+ { name: 'data', mountPath: d.storagePath },
187
+ { name: 'theme', mountPath: themeStagingPath, readOnly: true }
188
+ ],
189
+ resources: { requests: { cpu: '50m', memory: '32Mi' }, limits: { memory: '128Mi' } }
190
+ }
191
+ ],
192
+ containers: [{
193
+ name: d.id,
194
+ image: cfg.dockerImage,
195
+ ports: [
196
+ { name: 'http', containerPort: port },
197
+ { name: 'ssh', containerPort: cfg.sshPort, protocol: 'TCP' }
198
+ ],
199
+ envFrom: [{ secretRef: { name: secretName } }],
200
+ env: [
201
+ { name: 'USER_UID', value: '1000' },
202
+ { name: 'USER_GID', value: '1000' },
203
+ { name: 'GITEA__server__PROTOCOL', value: 'http' },
204
+ { name: 'GITEA__server__DOMAIN', value: cfg.domain },
205
+ { name: 'GITEA__server__ROOT_URL', value: `https://${cfg.domain}` },
206
+ { name: 'GITEA__server__HTTP_PORT', value: String(port) },
207
+ { name: 'GITEA__server__SSH_PORT', value: String(cfg.sshPort) },
208
+ { name: 'GITEA__DEFAULT__APP_NAME', value: 'Vibes Git' },
209
+ { name: 'GITEA__ui__THEMES', value: 'auto,gitea,arc-green' },
210
+ { name: 'GITEA__ui__DEFAULT_THEME', value: 'auto' },
211
+ { name: 'GITEA__security__INSTALL_LOCK', value: 'true' },
212
+ { name: 'GITEA__oauth2__CLIENT_ID', valueFrom: { secretKeyRef: { name: secretName, key: 'oauth2-client-id' } } },
213
+ { name: 'GITEA__oauth2__CLIENT_SECRET', valueFrom: { secretKeyRef: { name: secretName, key: 'oauth2-client-secret' } } },
214
+ { name: 'GITEA__security__JWT_SECRET', valueFrom: { secretKeyRef: { name: secretName, key: 'jwt-secret' } } },
215
+ { name: 'GITEA__mailer__FROM', valueFrom: { secretKeyRef: { name: 'shared-secrets', key: 'smtp-from' } } },
216
+ { name: 'GITEA__mailer__PASSWD', valueFrom: { secretKeyRef: { name: secretName, key: 'smtp-password' } } }
217
+ ],
218
+ volumeMounts: [{ name: 'data', mountPath: d.storagePath }],
219
+ livenessProbe: {
220
+ httpGet: { path: '/', port },
221
+ initialDelaySeconds: 60,
222
+ periodSeconds: 30
223
+ },
224
+ readinessProbe: {
225
+ httpGet: { path: '/api/healthz', port },
226
+ periodSeconds: 10
227
+ },
228
+ resources: {
229
+ requests: { cpu: '100m', memory: '128Mi' },
230
+ limits: { cpu: '1', memory: '1Gi' }
231
+ }
232
+ }],
233
+ volumes: [
234
+ { name: 'data', persistentVolumeClaim: { claimName: pvcName } },
235
+ {
236
+ name: 'theme',
237
+ configMap: {
238
+ name: themeCmName,
239
+ items: themeItems
240
+ }
241
+ }
242
+ ]
243
+ }
244
+ }
245
+ }
246
+ };
247
+
248
+ const service: K8sService = {
249
+ apiVersion: 'v1',
250
+ kind: 'Service',
251
+ metadata: { name, namespace: ns },
252
+ spec: {
253
+ type: 'ClusterIP',
254
+ selector: { app: d.id },
255
+ ports: [
256
+ { name: 'http', port, targetPort: port },
257
+ { name: 'ssh', port: cfg.sshPort, targetPort: cfg.sshPort }
258
+ ]
259
+ }
260
+ };
261
+
262
+ const pvc = {
263
+ apiVersion: 'v1',
264
+ kind: 'PersistentVolumeClaim',
265
+ metadata: { name: pvcName, namespace: ns, labels: { app: d.id } },
266
+ spec: {
267
+ accessModes: ['ReadWriteOnce'],
268
+ resources: { requests: { storage: `${cfg.volumeSizeGb}Gi` } }
269
+ }
270
+ };
271
+
272
+ const themeConfigMap = {
273
+ apiVersion: 'v1',
274
+ kind: 'ConfigMap',
275
+ metadata: { name: themeCmName, namespace: ns, labels: { app: d.id } },
276
+ data: themeData
277
+ };
278
+
279
+ const ingress = {
280
+ apiVersion: 'networking.k8s.io/v1',
281
+ kind: 'Ingress',
282
+ metadata: {
283
+ name,
284
+ namespace: ns,
285
+ labels: { app: d.id },
286
+ annotations: {
287
+ 'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
288
+ 'nginx.ingress.kubernetes.io/proxy-body-size': '100m'
289
+ }
290
+ },
291
+ spec: {
292
+ tls: [{ hosts: [cfg.domain], secretName: `${name}-tls` }],
293
+ rules: [{
294
+ host: cfg.domain,
295
+ http: {
296
+ paths: [{
297
+ path: '/',
298
+ pathType: 'Prefix' as const,
299
+ backend: { service: { name, port: { number: port } } }
300
+ }]
301
+ }
302
+ }]
303
+ }
304
+ };
305
+
306
+ return { deployment, service, pvc, themeConfigMap, ingress };
307
+ }
308
+
309
+ export function createGiteaImpl(input: GiteaDoksDescriptorInput) {
310
+ const descriptor = GiteaDoksDescriptorSchema.parse(input);
311
+ return createRuntimeImplementation({
312
+ id: 'gitea-doks',
313
+ kind: 'infra/git-hosting',
314
+ priority: 20,
315
+ implementation: {
316
+ generateConfig: () => generateGiteaManifest(descriptor)
317
+ }
318
+ });
319
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Implementation: infra/database -> do-managed-postgres
3
+ *
4
+ * Generates a DigitalOcean API-compatible managed database specification.
5
+ */
6
+
7
+ import * as z from 'zod/v4';
8
+ import { DOManagedPostgresConfigSchema } from '@vibesdotdev/infra-core/kinds';
9
+ import type { DOManagedDBSpec } from '../types.ts';
10
+
11
+ export const DOManagedPostgresDescriptorSchema = z.object({
12
+ kind: z.literal('infra/database'),
13
+ id: z.string().min(1),
14
+ name: z.string().optional(),
15
+ description: z.string().optional(),
16
+ engine: z.literal('postgres'),
17
+ replicas: z.number().int().nonnegative().default(0),
18
+ config: DOManagedPostgresConfigSchema.extend({
19
+ adapter: z.literal('do-managed-postgres')
20
+ })
21
+ });
22
+
23
+ export type DOManagedPostgresDescriptorInput = z.input<typeof DOManagedPostgresDescriptorSchema>;
24
+ export type DOManagedPostgresDescriptor = z.infer<typeof DOManagedPostgresDescriptorSchema>;
25
+
26
+ export function generateDOManagedDB(descriptor: DOManagedPostgresDescriptor): DOManagedDBSpec {
27
+ const parsed = DOManagedPostgresDescriptorSchema.parse(descriptor);
28
+ const cfg = parsed.config!;
29
+ return {
30
+ name: `vibes-${parsed.id}`,
31
+ engine: 'pg',
32
+ version: cfg.version,
33
+ size: cfg.size,
34
+ region: cfg.region,
35
+ num_nodes: (parsed.replicas ?? 0) + 1
36
+ };
37
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Implementation: infra/cache -> do-managed-redis
3
+ *
4
+ * Generates a DigitalOcean API-compatible managed Redis specification.
5
+ */
6
+
7
+ import * as z from 'zod/v4';
8
+ import { DOManagedRedisConfigSchema } from '@vibesdotdev/infra-core/kinds';
9
+ import type { DOManagedRedisSpec } from '../types.ts';
10
+
11
+ const EVICTION_POLICY_MAP: Record<string, string> = {
12
+ lru: 'allkeys-lru',
13
+ lfu: 'allkeys-lfu',
14
+ ttl: 'volatile-ttl'
15
+ };
16
+
17
+ export const DOManagedRedisDescriptorSchema = z.object({
18
+ kind: z.literal('infra/cache'),
19
+ id: z.string().min(1),
20
+ name: z.string().optional(),
21
+ description: z.string().optional(),
22
+ engine: z.literal('redis'),
23
+ maxMemory: z.string().min(1).optional(),
24
+ eviction: z.enum(['lru', 'lfu', 'ttl']).optional(),
25
+ config: DOManagedRedisConfigSchema.extend({
26
+ adapter: z.literal('do-managed-redis')
27
+ })
28
+ });
29
+
30
+ export type DOManagedRedisDescriptorInput = z.input<typeof DOManagedRedisDescriptorSchema>;
31
+ export type DOManagedRedisDescriptor = z.infer<typeof DOManagedRedisDescriptorSchema>;
32
+
33
+ export function generateDOManagedRedis(descriptor: DOManagedRedisDescriptor): DOManagedRedisSpec {
34
+ const parsed = DOManagedRedisDescriptorSchema.parse(descriptor);
35
+ const cfg = parsed.config!;
36
+ const size =
37
+ cfg.size ??
38
+ (parsed.maxMemory ? `db-s-1vcpu-${parsed.maxMemory}` : 'db-s-1vcpu-1gb');
39
+
40
+ return {
41
+ name: `vibes-${parsed.id}`,
42
+ engine: 'redis',
43
+ version: cfg.version,
44
+ size,
45
+ region: cfg.region,
46
+ num_nodes: cfg.numNodes,
47
+ eviction_policy: cfg.eviction ? EVICTION_POLICY_MAP[cfg.eviction] : undefined
48
+ };
49
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Implementation: infra/object-storage -> do-spaces
3
+ *
4
+ * Generates a DigitalOcean Spaces specification.
5
+ */
6
+
7
+ import * as z from 'zod/v4';
8
+ import {
9
+ DOSpacesConfigSchema,
10
+ CorsRuleSchema,
11
+ LifecycleRuleSchema
12
+ } from '@vibesdotdev/infra-core/kinds';
13
+ import type { DOSpacesSpec } from '../types.ts';
14
+
15
+ export const DOSpacesDescriptorSchema = z.object({
16
+ kind: z.literal('infra/object-storage'),
17
+ id: z.string().min(1),
18
+ name: z.string().optional(),
19
+ description: z.string().optional(),
20
+ publicAccess: z.boolean().default(false),
21
+ cors: z.array(CorsRuleSchema).optional(),
22
+ lifecycle: z
23
+ .array(
24
+ LifecycleRuleSchema
25
+ )
26
+ .optional(),
27
+ config: DOSpacesConfigSchema.extend({
28
+ adapter: z.literal('do-spaces')
29
+ })
30
+ });
31
+
32
+ export type DOSpacesDescriptorInput = z.input<typeof DOSpacesDescriptorSchema>;
33
+ export type DOSpacesDescriptor = z.infer<typeof DOSpacesDescriptorSchema>;
34
+
35
+ export function generateDOSpaces(descriptor: DOSpacesDescriptor): DOSpacesSpec {
36
+ const parsed = DOSpacesDescriptorSchema.parse(descriptor);
37
+ return {
38
+ name: `vibes-${parsed.id}`,
39
+ region: parsed.config?.region ?? 'nyc3',
40
+ acl: parsed.publicAccess ? 'public-read' : 'private',
41
+ cors_rules: parsed.cors?.map((rule) => ({
42
+ allowed_origins: rule.allowedOrigins,
43
+ allowed_methods: rule.allowedMethods,
44
+ allowed_headers: rule.allowedHeaders,
45
+ max_age_seconds: rule.maxAgeSeconds
46
+ })),
47
+ lifecycle_rules: parsed.lifecycle?.map((rule) => ({
48
+ prefix: rule.prefix,
49
+ expiration_days: rule.expiration
50
+ }))
51
+ };
52
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Implementation: infra/queue (Kafka/Redpanda) and infra/database (self-hosted PG)
3
+ * when adapter is 'doks-statefulset'.
4
+ *
5
+ * Generates a K8s StatefulSet with PersistentVolumeClaim templates.
6
+ */
7
+
8
+ import * as z from 'zod/v4';
9
+ import {
10
+ DoksQueueConfigSchema,
11
+ DoksDatabaseConfigSchema
12
+ } from '@vibesdotdev/infra-core/kinds';
13
+ import type { K8sStatefulSet } from '../types.ts';
14
+
15
+ export const DoksQueueDescriptorSchema = z.object({
16
+ kind: z.literal('infra/queue'),
17
+ id: z.string().min(1),
18
+ name: z.string().optional(),
19
+ description: z.string().optional(),
20
+ engine: z.enum(['redpanda', 'kafka']),
21
+ config: DoksQueueConfigSchema.extend({
22
+ adapter: z.literal('doks-statefulset')
23
+ })
24
+ });
25
+
26
+ export type DoksQueueDescriptorInput = z.input<typeof DoksQueueDescriptorSchema>;
27
+ export type DoksQueueDescriptor = z.infer<typeof DoksQueueDescriptorSchema>;
28
+
29
+ export const DoksDatabaseDescriptorSchema = z.object({
30
+ kind: z.literal('infra/database'),
31
+ id: z.string().min(1),
32
+ name: z.string().optional(),
33
+ description: z.string().optional(),
34
+ engine: z.literal('postgres'),
35
+ replicas: z.number().int().nonnegative().default(0),
36
+ config: DoksDatabaseConfigSchema.extend({
37
+ adapter: z.literal('doks-statefulset')
38
+ })
39
+ });
40
+
41
+ export type DoksDatabaseDescriptorInput = z.input<typeof DoksDatabaseDescriptorSchema>;
42
+ export type DoksDatabaseDescriptor = z.infer<typeof DoksDatabaseDescriptorSchema>;
43
+
44
+ type QueueOrDatabaseDescriptor = DoksQueueDescriptor | DoksDatabaseDescriptor;
45
+
46
+ export function generateK8sStatefulSet(descriptor: QueueOrDatabaseDescriptor): K8sStatefulSet {
47
+ if (descriptor.kind === 'infra/queue') {
48
+ return generateQueueStatefulSet(descriptor as DoksQueueDescriptor);
49
+ }
50
+ return generateDatabaseStatefulSet(descriptor as DoksDatabaseDescriptor);
51
+ }
52
+
53
+ function generateQueueStatefulSet(descriptor: DoksQueueDescriptor): K8sStatefulSet {
54
+ const parsed = DoksQueueDescriptorSchema.parse(descriptor);
55
+ const image =
56
+ parsed.engine === 'redpanda'
57
+ ? 'vectorized/redpanda:latest'
58
+ : 'confluentinc/cp-kafka:latest';
59
+
60
+ const port = 9092;
61
+ const namespace = parsed.config?.namespace ?? 'vibes';
62
+ const storageSize = parsed.config?.storageSize ?? '10Gi';
63
+ const defaultRequests = { cpu: '250m', memory: '512Mi' };
64
+ const defaultLimits = { cpu: '1', memory: '1Gi' };
65
+
66
+ return {
67
+ apiVersion: 'apps/v1',
68
+ kind: 'StatefulSet',
69
+ metadata: {
70
+ name: `vibes-${parsed.id}`,
71
+ namespace,
72
+ labels: { app: parsed.id, 'app.kubernetes.io/part-of': 'vibes' }
73
+ },
74
+ spec: {
75
+ serviceName: `vibes-${parsed.id}`,
76
+ replicas: 1,
77
+ selector: { matchLabels: { app: parsed.id } },
78
+ template: {
79
+ metadata: { labels: { app: parsed.id } },
80
+ spec: {
81
+ containers: [
82
+ {
83
+ name: parsed.id,
84
+ image,
85
+ ports: [{ name: 'kafka', containerPort: port, protocol: 'TCP' }],
86
+ resources: {
87
+ requests: parsed.config?.resources?.requests ?? defaultRequests,
88
+ limits: parsed.config?.resources?.limits ?? defaultLimits
89
+ },
90
+ volumeMounts: [{ name: 'data', mountPath: '/var/lib/data' }],
91
+ livenessProbe: {
92
+ tcpSocket: { port },
93
+ periodSeconds: 30
94
+ },
95
+ readinessProbe: {
96
+ tcpSocket: { port },
97
+ periodSeconds: 10
98
+ }
99
+ }
100
+ ]
101
+ }
102
+ },
103
+ volumeClaimTemplates: [
104
+ {
105
+ metadata: { name: 'data' },
106
+ spec: {
107
+ accessModes: ['ReadWriteOnce'],
108
+ resources: { requests: { storage: storageSize } }
109
+ }
110
+ }
111
+ ]
112
+ }
113
+ };
114
+ }
115
+
116
+ function generateDatabaseStatefulSet(descriptor: DoksDatabaseDescriptor): K8sStatefulSet {
117
+ const parsed = DoksDatabaseDescriptorSchema.parse(descriptor);
118
+ const storageSize = parsed.config?.storageSize ?? '10Gi';
119
+ const namespace = parsed.config?.namespace ?? 'vibes';
120
+ const totalReplicas = parsed.replicas + 1;
121
+ const version = parsed.config?.version ?? '16-alpine';
122
+ const image = `postgres:${version}`;
123
+ const defaultRequests = { cpu: '250m', memory: '256Mi' };
124
+ const defaultLimits = { cpu: '1', memory: '1Gi' };
125
+
126
+ return {
127
+ apiVersion: 'apps/v1',
128
+ kind: 'StatefulSet',
129
+ metadata: {
130
+ name: `vibes-${parsed.id}`,
131
+ namespace,
132
+ labels: { app: parsed.id, 'app.kubernetes.io/part-of': 'vibes' }
133
+ },
134
+ spec: {
135
+ serviceName: `vibes-${parsed.id}`,
136
+ replicas: totalReplicas,
137
+ selector: { matchLabels: { app: parsed.id } },
138
+ template: {
139
+ metadata: { labels: { app: parsed.id } },
140
+ spec: {
141
+ containers: [
142
+ {
143
+ name: parsed.id,
144
+ image,
145
+ ports: [{ name: 'postgres', containerPort: 5432, protocol: 'TCP' }],
146
+ env: [
147
+ { name: 'POSTGRES_DB', value: `vibes_${parsed.id}` },
148
+ {
149
+ name: 'POSTGRES_PASSWORD',
150
+ valueFrom: {
151
+ secretKeyRef: {
152
+ name: `vibes-${parsed.id}-credentials`,
153
+ key: 'password'
154
+ }
155
+ }
156
+ }
157
+ ],
158
+ resources: {
159
+ requests: parsed.config?.resources?.requests ?? defaultRequests,
160
+ limits: parsed.config?.resources?.limits ?? defaultLimits
161
+ },
162
+ volumeMounts: [{ name: 'pgdata', mountPath: '/var/lib/postgresql/data' }],
163
+ livenessProbe: {
164
+ exec: { command: ['pg_isready', '-U', 'postgres'] },
165
+ periodSeconds: 30
166
+ },
167
+ readinessProbe: {
168
+ exec: { command: ['pg_isready', '-U', 'postgres'] },
169
+ periodSeconds: 10
170
+ }
171
+ }
172
+ ]
173
+ }
174
+ },
175
+ volumeClaimTemplates: [
176
+ {
177
+ metadata: { name: 'pgdata' },
178
+ spec: {
179
+ accessModes: ['ReadWriteOnce'],
180
+ resources: { requests: { storage: storageSize } }
181
+ }
182
+ }
183
+ ]
184
+ }
185
+ };
186
+ }