Repository navigation
feat(orchestrator): add support for Docker Compose project deployments #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
76455bd
11edfbf
bbad8cc
027e32d
e4f0b95
9eb6a5b
00ab762
1dd87b9
df031b2
f2c6846
4d22d8a
5b37f3a
97d88cf
c7e4873
a3f06a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,3 +14,5 @@ bump.sh | |
| scripts/workflow/bump.sh | ||
| __pycache__ | ||
| .tegami/changes-* | ||
| SETUP_GUIDE.md | ||
| bun.lock | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,59 +24,64 @@ const tryRun = (cmd: string, args: string[]) => | |||||||||||||||||||||||||||||||||||||||||||||||||||
| export const provisionDatabase = async (dbRecord: Database): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const containerName = dbRecord.internalHost; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const version = dbRecord.version || (dbRecord.type === 'mysql' ? '8.0' : '16-alpine'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const image = dbRecord.type === 'mysql' ? `mysql:${version}` : `postgres:${version}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const version = dbRecord.version || (dbRecord.type === 'mysql' ? '8.0' : '16-alpine'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const image = dbRecord.type === 'mysql' ? `mysql:${version}` : `postgres:${version}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Pull image first (non-blocking) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await tryRun(dockerBin, ['pull', image]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await tryRun(dockerBin, ['pull', image]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const envVars = dbRecord.type === 'mysql' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_ROOT_PASSWORD=${dbRecord.password}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_DATABASE=${dbRecord.databaseName}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_USER=${dbRecord.username}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_PASSWORD=${dbRecord.password}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `POSTGRES_USER=${dbRecord.username}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `POSTGRES_PASSWORD=${dbRecord.password}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `POSTGRES_DB=${dbRecord.databaseName}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const envVars = dbRecord.type === 'mysql' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_ROOT_PASSWORD=${dbRecord.password}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_DATABASE=${dbRecord.databaseName}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_USER=${dbRecord.username}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `MYSQL_PASSWORD=${dbRecord.password}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `POSTGRES_USER=${dbRecord.username}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `POSTGRES_PASSWORD=${dbRecord.password}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `POSTGRES_DB=${dbRecord.databaseName}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const volumeName = `db-${dbRecord.id.slice(0, 12)}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const volumeName = `db-${dbRecord.id.slice(0, 12)}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create volume | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await tryRun(dockerBin, ['volume', 'create', volumeName]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await tryRun(dockerBin, ['volume', 'create', volumeName]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await tryRun(dockerBin, ['rm', '-f', containerName]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const args = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'run', '-d', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '--name', containerName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '--network', config.dockerNetwork, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '--network-alias', containerName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '-l', DEQUEL_MANAGED_LABEL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(dbRecord.cpuLimit ? ['--cpus', String(dbRecord.cpuLimit)] : []), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(dbRecord.memoryLimitMb ? ['--memory', `${Math.round(dbRecord.memoryLimitMb)}m`] : []), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '-v', `${volumeName}:/var/lib/${dbRecord.type === 'mysql' ? 'mysql' : 'postgresql/data'}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '-e', `TZ=UTC`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...envVars.flatMap(e => ['-e', e]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| image, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const args = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'run', '-d', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '--name', containerName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '--network', config.dockerNetwork, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '--network-alias', containerName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '-l', DEQUEL_MANAGED_LABEL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(dbRecord.cpuLimit ? ['--cpus', String(dbRecord.cpuLimit)] : []), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(dbRecord.memoryLimitMb ? ['--memory', `${Math.round(dbRecord.memoryLimitMb)}m`] : []), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '-v', `${volumeName}:/var/lib/${dbRecord.type === 'mysql' ? 'mysql' : 'postgresql/data'}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| '-e', `TZ=UTC`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...envVars.flatMap(e => ['-e', e]), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| image, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| await run(dockerBin, args); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await run(dockerBin, args); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Wait for healthy | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < 30; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (status.trim() === 'running') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await updateDatabaseStatus(dbRecord.id, 'running', containerName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await new Promise(r => setTimeout(r, 2000)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < 30; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = await run(dockerBin, ['inspect', '-f', '{{.State.Status}}', containerName]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (status.trim() === 'running') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await updateDatabaseStatus(dbRecord.id, 'running', containerName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await new Promise(r => setTimeout(r, 2000)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| await updateDatabaseStatus(dbRecord.id, 'failed', containerName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await updateDatabaseStatus(dbRecord.id, 'failed', containerName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Fail the provisioning operation after the readiness timeout. At Line 78, the function records Throw a timeout error here. The existing Proposed fix- await updateDatabaseStatus(dbRecord.id, 'failed', containerName);
+ throw new Error(`Database ${dbRecord.id} did not reach running state before the readiness timeout`);📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.45.0)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Database ${containerName} failed to become ready within 60 seconds`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`[DB Provisioner] Error provisioning database ${dbRecord.id}:`, err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await updateDatabaseStatus(dbRecord.id, 'failed', containerName).catch(() => {}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw err; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const deprovisionDatabase = async (dbRecord: Database): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ALTER TABLE `projects` ADD `build_type` text DEFAULT 'railpack' NOT NULL;--> statement-breakpoint | ||
| ALTER TABLE `projects` ADD `compose_service` text;--> statement-breakpoint | ||
| ALTER TABLE `projects` ADD `compose_port` integer; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| ALTER TABLE `domains` ADD `target_service` text;--> statement-breakpoint | ||
| ALTER TABLE `domains` ADD `target_port` integer; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ALTER TABLE `projects` ADD `compose_services` text; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Preserve target routing for domains verified after deployment.
This route persists
targetServiceandtargetPort, but the verification flow callsaddToCaddyRoutewithout either value.addToCaddyRouteonly appends the domain to the primary Caddy block. A target domain added after deployment therefore routes to the default container and port until a later redeploy regenerates the snippet.Update
addToCaddyRouteand both verification call sites to generate a dedicated proxy block from the persistedDomain, or regenerate the project snippet from persisted domain records.🤖 Prompt for AI Agents