diff --git a/backend/src/build/build.service.ts b/backend/src/build/build.service.ts index 6c8d056..78527fd 100644 --- a/backend/src/build/build.service.ts +++ b/backend/src/build/build.service.ts @@ -385,55 +385,20 @@ export class BuildService { fs.writeFileSync(tmpKubeconfig, kcYaml); try { - // 5. Stream the zip into the helper pod via kubectl exec + stdin. - // We use the @kubernetes/client-node Exec API with WebSocket for reliable binary streaming. + // 5. Copy the zip into the helper pod via kubectl cp. + // Both k8s.Exec (WebSocket) and kubectl exec -i stdin piping are unreliable + // for binary transfers — data can be lost before the connection is ready. + // kubectl cp uses tar internally and handles connection timing correctly. const t2 = Date.now(); - const exec = new k8s.Exec(kc); - const { Writable, Readable } = require('stream'); + await execFileAsync('kubectl', [ + '--kubeconfig', tmpKubeconfig, + 'cp', zipPath, + `${namespace}/${helperPodName}:/data/source.zip`, + '-c', 'helper', + ], { timeout: 1200_000 }); - // Null writable to discard stdout/stderr - const devNull = new Writable({ write(_c: any, _e: any, cb: any) { cb(); } }); - - await new Promise((resolve, reject) => { - const fileStream = fs.createReadStream(zipPath, { highWaterMark: 256 * 1024 }); - - let resolved = false; - const done = (err?: Error) => { - if (resolved) return; - resolved = true; - if (err) reject(err); else resolve(); - }; - - // Safety timeout - const timer = setTimeout(() => done(new Error( - `K8s exec upload timed out after 20 minutes for ${(zipSize / 1024 / 1024).toFixed(1)} MB`, - )), 1200_000); - - exec.exec( - namespace, - helperPodName, - 'helper', - ['sh', '-c', 'cat > /data/source.zip'], - devNull, // stdout - devNull, // stderr - fileStream, // stdin - false, // tty - (status: k8s.V1Status) => { - clearTimeout(timer); - if (status.status === 'Success') { - done(); - } else { - done(new Error(`K8s exec failed: ${status.message || status.reason || 'unknown'}`)); - } - }, - ).catch((err: Error) => { - clearTimeout(timer); - done(err); - }); - }); - - this.logger.log(`[timing] K8s exec upload completed in ${Date.now() - t2}ms (${(zipSize / 1024 / 1024).toFixed(1)} MB)`); + this.logger.log(`[timing] kubectl cp upload completed in ${Date.now() - t2}ms (${(zipSize / 1024 / 1024).toFixed(1)} MB)`); // 5b. Verify the file was written correctly const { stdout: sizeStr } = await execFileAsync('kubectl', [ @@ -1053,7 +1018,7 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\ CMD curl -f http://localhost:${port}/health || exit 1 # Auto-detect: Flask, FastAPI, or plain Python -CMD ["sh", "-c", "if [ -f main.py ]; then if grep -q 'FastAPI\\|fastapi' main.py; then uvicorn main:app --host 0.0.0.0 --port ${port}; elif grep -q 'Flask\\|flask' main.py; then gunicorn -w 4 -b 0.0.0.0:${port} main:app; else python main.py; fi; elif [ -f app.py ]; then if grep -q 'FastAPI\\|fastapi' app.py; then uvicorn app:app --host 0.0.0.0 --port ${port}; elif grep -q 'Flask\\|flask' app.py; then gunicorn -w 4 -b 0.0.0.0:${port} app:app; else python app.py; fi; else gunicorn -w 4 -b 0.0.0.0:${port} app:app; fi"] +CMD sh -c "if [ -f main.py ]; then if grep -qi fastapi main.py; then exec uvicorn main:app --host 0.0.0.0 --port ${port}; elif grep -qi flask main.py; then exec gunicorn -w 4 -b 0.0.0.0:${port} main:app; else exec python main.py; fi; elif [ -f app.py ]; then if grep -qi fastapi app.py; then exec uvicorn app:app --host 0.0.0.0 --port ${port}; elif grep -qi flask app.py; then exec gunicorn -w 4 -b 0.0.0.0:${port} app:app; else exec python app.py; fi; else exec gunicorn -w 4 -b 0.0.0.0:${port} app:app; fi" `; } @@ -1108,14 +1073,14 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \\ CMD curl -f http://localhost:${port}/health/ || curl -f http://localhost:${port}/ || exit 1 # Auto-detect project structure and run migrations + collectstatic -CMD ["sh", "-c", "\\ - PROJECT_NAME=$(find . -maxdepth 2 -name 'wsgi.py' | head -1 | cut -d'/' -f2) && \\ - if [ -z \\\"$PROJECT_NAME\\\" ]; then PROJECT_NAME='config'; fi && \\ - echo \\\"Django project: $PROJECT_NAME\\\" && \\ +CMD sh -c "\\ + PROJECT_NAME=\\$(find . -maxdepth 2 -name 'wsgi.py' | head -1 | cut -d'/' -f2) && \\ + if [ -z \\\"\\$PROJECT_NAME\\\" ]; then PROJECT_NAME='config'; fi && \\ + echo \\\"Django project: \\$PROJECT_NAME\\\" && \\ python manage.py migrate --noinput 2>/dev/null || true && \\ python manage.py collectstatic --noinput 2>/dev/null || true && \\ - gunicorn $PROJECT_NAME.wsgi:application --bind 0.0.0.0:${port} --workers 4 --threads 2 \\ -"] + exec gunicorn \\$PROJECT_NAME.wsgi:application --bind 0.0.0.0:${port} --workers 4 --threads 2 \\ +" `; } @@ -1176,8 +1141,19 @@ CMD ["sh", "-c", "DLL=$(find . -maxdepth 1 -name '*.dll' ! -name '*.deps.dll' ! let lastLoggedStatus = ''; while (Date.now() - startTime < timeoutMs) { - // ── Check Job status ── - const job = await batchApi.readNamespacedJob(jobName, namespace); + // ── Check Job status (with retry for transient connection errors) ── + let job: { body: k8s.V1Job }; + try { + job = await batchApi.readNamespacedJob(jobName, namespace); + } catch (pollErr: any) { + const code = pollErr?.code || pollErr?.message || ''; + if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|socket hang up/i.test(String(code))) { + this.logger.warn(`Transient K8s API error polling job ${jobName}: ${code} — retrying in 5s`); + await new Promise(r => setTimeout(r, 5000)); + continue; + } + throw pollErr; + } const status = job.body.status; if (status?.succeeded && status.succeeded > 0) { diff --git a/backend/src/seed.ts b/backend/src/seed.ts index a124753..1ff3585 100644 --- a/backend/src/seed.ts +++ b/backend/src/seed.ts @@ -14,14 +14,14 @@ import { UserRole } from './common/enums'; * npm run seed * * Environment variables (or defaults): - * ADMIN_EMAIL=admin@cloudhost.local + * ADMIN_EMAIL=test@example.com * ADMIN_PASSWORD=Admin123! */ async function bootstrap() { const app = await NestFactory.createApplicationContext(AppModule); const usersService = app.get(UsersService); - const email = process.env.ADMIN_EMAIL || 'admin@cloudhost.local'; + const email = process.env.ADMIN_EMAIL || 'test@example.com'; const password = process.env.ADMIN_PASSWORD || 'Admin123!'; const existing = await usersService.findByEmail(email);