feat(core-docker): modulo docker com detector de stacks, gerador de compose e dockerfile para 10 stacks

- detector de stacks por presenca de arquivos
- gerador de docker-compose.yml Scenario A + Scenario C
- gerador de Dockerfiles para: nodejs, nextjs, nestjs, nuxt, laravel, php-generic, python, go, rust, wordpress
- validador xCloud: build:, proxy conflicts, multi-porta, healthcheck
- barrel exports em src/docker/index.ts
- organizado por domain-driven design
This commit is contained in:
pulse-agent
2026-05-19 23:02:16 -03:00
parent e3fedd4582
commit 2855032e76
7 changed files with 536 additions and 0 deletions
@@ -0,0 +1,103 @@
/**
* compose.ts — Gerador de Docker Compose xCloud-ready
* Scenario A: build-source (1 app + opcional db)
* Scenario C: multi-service (vários apps + nginx-router)
*/
import type { DockerCompose } from './types';
const GHCR = (owner: string, repo: string) => `ghcr.io/${owner}/${repo}`;
const DB = { postgres: 'postgres:16-alpine', mysql: 'mysql:8.0' };
/** Scenario A */
export function scenarioA(opts: {
owner: string; repo: string; service: string; port: number;
dbType: 'postgres' | 'mysql' | 'none'; env: Record<string, string>;
}): DockerCompose {
const img = `${GHCR(opts.owner, opts.repo)}:latest`;
const svcs: Record<string, any> = {
app: { image: img, ports: [`${opts.port}:3000`], environment: opts.env, networks: ['app-net'] },
};
if (opts.dbType !== 'none') {
const isPg = opts.dbType === 'postgres';
svcs['db'] = {
image: DB[opts.dbType],
expose: [isPg ? '5432' : '3306'],
environment: isPg
? { POSTGRES_DB: '${POSTGRES_DB}', POSTGRES_USER: '${POSTGRES_USER}', POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}' }
: { MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}', MYSQL_DATABASE: '${DB_DATABASE}', MYSQL_USER: '${DB_USERNAME}', MYSQL_PASSWORD: '${DB_PASSWORD}' },
volumes: ['db-data:/var/lib/postgresql/data'],
healthcheck: { test: isPg ? ['CMD-SHELL','pg_isready -U ${POSTGRES_USER}'] : ['CMD','mysqladmin','ping','-h','localhost'], interval: '10s', timeout: '5s', retries: 5 },
networks: ['app-net'],
};
svcs['app'].depends_on = [{ service: 'db', condition: 'service_healthy' }];
}
return { services: svcs, ...(opts.dbType !== 'none' ? { volumes: { 'db-data': {} } } : {}), networks: { 'app-net': { driver: 'bridge' } } };
}
/** Scenario C — multi-service com nginx-router */
export function scenarioC(opts: {
owner: string; repo: string;
services: Array<{ name: string; port: number; internalPort: number }>;
dbType: 'postgres' | 'mysql' | 'none';
envs: Record<string, Record<string, string>>;
}): DockerCompose {
const svcs: Record<string, any> = {};
const base = GHCR(opts.owner, opts.repo);
for (const s of opts.services) {
svcs[s.name] = { image: `${base}/${s.name}:latest`, expose: [String(s.internalPort)], environment: opts.envs[s.name] || {}, networks: ['app-net'] };
}
if (opts.dbType !== 'none') {
const isPg = opts.dbType === 'postgres';
svcs['db'] = {
image: DB[opts.dbType], expose: [isPg ? '5432' : '3306'],
environment: { POSTGRES_PASSWORD: '${POSTGRES_PASSWORD}' },
healthcheck: { test: isPg ? ['CMD-SHELL','pg_isready -U ${POSTGRES_USER}'] : ['CMD','mysqladmin','ping','-h','localhost'], interval: '10s', timeout: '5s', retries: 5 },
volumes: ['db-data:/var/lib/postgresql/data'], networks: ['app-net'],
};
}
// nginx-router única porta externa para xCloud
const extPort = opts.services[0]?.port || 3080;
svcs['nginx-router'] = {
image: 'nginx:alpine', ports: [`${extPort}:80`],
configs: [{ source: 'nginx_cfg', target: '/etc/nginx/conf.d/default.conf' }],
depends_on: opts.services.map(s => ({ service: s.name })),
networks: ['app-net'],
};
const back = opts.services.find(s => s.name !== 'frontend');
const front = opts.services.find(s => s.name !== 'backend');
const bPort = back?.internalPort || 8000;
const fPort = front?.port || opts.services[0]?.port || 3000;
return {
services: svcs,
configs: {
nginx_cfg: {
content: `upstream backend { server backend:${bPort}; }\nupstream frontend { server frontend:${fPort}; }\n\nserver {\n listen 80;\n\n location /api/ { proxy_pass http://backend:${bPort}/; }\n location / { proxy_pass http://frontend:${fPort}/; }\n}\n`,
},
},
volumes: opts.dbType !== 'none' ? { 'db-data': {} } : undefined,
networks: { 'app-net': { driver: 'bridge' } },
};
}
// Helper para nginx-router para adicionar a compose existente
export function addNginxRouter(svcs: Record<string, any>, externalPort: number = 3080): { configs: Record<string,{content:string}> } {
if (svcs['nginx-router']) return { configs: {} };
svcs['nginx-router'] = {
image: 'nginx:alpine', ports: [`${externalPort}:80`],
depends_on: Object.keys(svcs).filter(k => k !== 'nginx-router'),
networks: ['app-net'],
};
return {
configs: {
nginx_cfg: {
content: `server {\n listen 80;\n location / { proxy_pass http://frontend:3000/; }\n}\n`,
},
},
};
}