@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.
- package/README.md +107 -0
- package/SPEC.md +285 -0
- package/dist/client/digitalocean-app-deploy.client.d.ts +46 -0
- package/dist/client/digitalocean-app-deploy.client.d.ts.map +1 -0
- package/dist/client/digitalocean-app-deploy.client.js +135 -0
- package/dist/client/digitalocean-app-deploy.client.js.map +1 -0
- package/dist/client/index.d.ts +15 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/index.js.map +1 -0
- package/dist/cloud/base.d.ts +33 -0
- package/dist/cloud/base.d.ts.map +1 -0
- package/dist/cloud/base.js +86 -0
- package/dist/cloud/base.js.map +1 -0
- package/dist/cloud/digitalocean.d.ts +33 -0
- package/dist/cloud/digitalocean.d.ts.map +1 -0
- package/dist/cloud/digitalocean.js +258 -0
- package/dist/cloud/digitalocean.js.map +1 -0
- package/dist/cloud/factory.d.ts +28 -0
- package/dist/cloud/factory.d.ts.map +1 -0
- package/dist/cloud/factory.js +151 -0
- package/dist/cloud/factory.js.map +1 -0
- package/dist/cloud/index.d.ts +12 -0
- package/dist/cloud/index.d.ts.map +1 -0
- package/dist/cloud/index.js +11 -0
- package/dist/cloud/index.js.map +1 -0
- package/dist/doks.plugin.d.ts +41 -0
- package/dist/doks.plugin.d.ts.map +1 -0
- package/dist/doks.plugin.js +287 -0
- package/dist/doks.plugin.js.map +1 -0
- package/dist/implementations/deployment.impl.d.ts +34 -0
- package/dist/implementations/deployment.impl.d.ts.map +1 -0
- package/dist/implementations/deployment.impl.js +86 -0
- package/dist/implementations/deployment.impl.js.map +1 -0
- package/dist/implementations/droplet.impl.d.ts +85 -0
- package/dist/implementations/droplet.impl.d.ts.map +1 -0
- package/dist/implementations/droplet.impl.js +113 -0
- package/dist/implementations/droplet.impl.js.map +1 -0
- package/dist/implementations/gitea.impl.d.ts +68 -0
- package/dist/implementations/gitea.impl.d.ts.map +1 -0
- package/dist/implementations/gitea.impl.js +295 -0
- package/dist/implementations/gitea.impl.js.map +1 -0
- package/dist/implementations/managed-db.impl.d.ts +25 -0
- package/dist/implementations/managed-db.impl.d.ts.map +1 -0
- package/dist/implementations/managed-db.impl.js +31 -0
- package/dist/implementations/managed-db.impl.js.map +1 -0
- package/dist/implementations/managed-redis.impl.d.ts +37 -0
- package/dist/implementations/managed-redis.impl.d.ts.map +1 -0
- package/dist/implementations/managed-redis.impl.js +40 -0
- package/dist/implementations/managed-redis.impl.js.map +1 -0
- package/dist/implementations/spaces.impl.d.ts +36 -0
- package/dist/implementations/spaces.impl.d.ts.map +1 -0
- package/dist/implementations/spaces.impl.js +40 -0
- package/dist/implementations/spaces.impl.js.map +1 -0
- package/dist/implementations/statefulset.impl.d.ts +65 -0
- package/dist/implementations/statefulset.impl.d.ts.map +1 -0
- package/dist/implementations/statefulset.impl.js +165 -0
- package/dist/implementations/statefulset.impl.js.map +1 -0
- package/dist/implementations/verdaccio.impl.d.ts +65 -0
- package/dist/implementations/verdaccio.impl.d.ts.map +1 -0
- package/dist/implementations/verdaccio.impl.js +259 -0
- package/dist/implementations/verdaccio.impl.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/kubernetes/index.d.ts +95 -0
- package/dist/kubernetes/index.d.ts.map +1 -0
- package/dist/kubernetes/index.js +625 -0
- package/dist/kubernetes/index.js.map +1 -0
- package/dist/secrets/index.d.ts +4 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +4 -0
- package/dist/secrets/index.js.map +1 -0
- package/dist/secrets/vault.descriptor.d.ts +10 -0
- package/dist/secrets/vault.descriptor.d.ts.map +1 -0
- package/dist/secrets/vault.descriptor.js +25 -0
- package/dist/secrets/vault.descriptor.js.map +1 -0
- package/dist/secrets/vault.impl.cloud.d.ts +40 -0
- package/dist/secrets/vault.impl.cloud.d.ts.map +1 -0
- package/dist/secrets/vault.impl.cloud.js +178 -0
- package/dist/secrets/vault.impl.cloud.js.map +1 -0
- package/dist/secrets/vault.impl.d.ts +29 -0
- package/dist/secrets/vault.impl.d.ts.map +1 -0
- package/dist/secrets/vault.impl.js +137 -0
- package/dist/secrets/vault.impl.js.map +1 -0
- package/dist/types.d.ts +509 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +47 -0
- package/dist/types.js.map +1 -0
- package/package.json +145 -0
- package/src/client/digitalocean-app-deploy.client.ts +226 -0
- package/src/client/index.ts +24 -0
- package/src/cloud/base.ts +149 -0
- package/src/cloud/digitalocean.ts +363 -0
- package/src/cloud/factory.ts +190 -0
- package/src/cloud/index.ts +81 -0
- package/src/doks.plugin.ts +401 -0
- package/src/implementations/deployment.impl.ts +93 -0
- package/src/implementations/droplet.impl.ts +157 -0
- package/src/implementations/gitea.impl.ts +319 -0
- package/src/implementations/managed-db.impl.ts +37 -0
- package/src/implementations/managed-redis.impl.ts +49 -0
- package/src/implementations/spaces.impl.ts +52 -0
- package/src/implementations/statefulset.impl.ts +186 -0
- package/src/implementations/verdaccio.impl.ts +300 -0
- package/src/index.ts +136 -0
- package/src/kubernetes/index.ts +754 -0
- package/src/secrets/index.ts +9 -0
- package/src/secrets/vault.descriptor.ts +28 -0
- package/src/secrets/vault.impl.cloud.ts +278 -0
- package/src/secrets/vault.impl.ts +149 -0
- 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
|
+
}
|