fix(build): use kubectl cp for reliable source upload and fix Python/Django CMD

- Replace k8s.Exec WebSocket with kubectl cp for binary file transfer to PVC
- Fix Python Dockerfile CMD: switch from JSON exec form to shell form to avoid
  invalid JSON escape sequences (\|) causing shell parse errors
- Fix Django Dockerfile CMD with same shell form approach
- Add retry logic for ECONNRESET during build job polling
- Update seed.ts default admin email to match actual database

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-05-14 00:10:04 +03:30
parent 9937ee457d
commit d87b50c6a4
2 changed files with 33 additions and 57 deletions
+31 -55
View File
@@ -385,55 +385,20 @@ export class BuildService {
fs.writeFileSync(tmpKubeconfig, kcYaml); fs.writeFileSync(tmpKubeconfig, kcYaml);
try { try {
// 5. Stream the zip into the helper pod via kubectl exec + stdin. // 5. Copy the zip into the helper pod via kubectl cp.
// We use the @kubernetes/client-node Exec API with WebSocket for reliable binary streaming. // 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 t2 = Date.now();
const exec = new k8s.Exec(kc); await execFileAsync('kubectl', [
const { Writable, Readable } = require('stream'); '--kubeconfig', tmpKubeconfig,
'cp', zipPath,
`${namespace}/${helperPodName}:/data/source.zip`,
'-c', 'helper',
], { timeout: 1200_000 });
// Null writable to discard stdout/stderr this.logger.log(`[timing] kubectl cp upload completed in ${Date.now() - t2}ms (${(zipSize / 1024 / 1024).toFixed(1)} MB)`);
const devNull = new Writable({ write(_c: any, _e: any, cb: any) { cb(); } });
await new Promise<void>((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)`);
// 5b. Verify the file was written correctly // 5b. Verify the file was written correctly
const { stdout: sizeStr } = await execFileAsync('kubectl', [ 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 CMD curl -f http://localhost:${port}/health || exit 1
# Auto-detect: Flask, FastAPI, or plain Python # 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 CMD curl -f http://localhost:${port}/health/ || curl -f http://localhost:${port}/ || exit 1
# Auto-detect project structure and run migrations + collectstatic # Auto-detect project structure and run migrations + collectstatic
CMD ["sh", "-c", "\\ CMD sh -c "\\
PROJECT_NAME=$(find . -maxdepth 2 -name 'wsgi.py' | head -1 | cut -d'/' -f2) && \\ PROJECT_NAME=\\$(find . -maxdepth 2 -name 'wsgi.py' | head -1 | cut -d'/' -f2) && \\
if [ -z \\\"$PROJECT_NAME\\\" ]; then PROJECT_NAME='config'; fi && \\ if [ -z \\\"\\$PROJECT_NAME\\\" ]; then PROJECT_NAME='config'; fi && \\
echo \\\"Django project: $PROJECT_NAME\\\" && \\ echo \\\"Django project: \\$PROJECT_NAME\\\" && \\
python manage.py migrate --noinput 2>/dev/null || true && \\ python manage.py migrate --noinput 2>/dev/null || true && \\
python manage.py collectstatic --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 = ''; let lastLoggedStatus = '';
while (Date.now() - startTime < timeoutMs) { while (Date.now() - startTime < timeoutMs) {
// ── Check Job status ── // ── Check Job status (with retry for transient connection errors) ──
const job = await batchApi.readNamespacedJob(jobName, namespace); 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; const status = job.body.status;
if (status?.succeeded && status.succeeded > 0) { if (status?.succeeded && status.succeeded > 0) {
+2 -2
View File
@@ -14,14 +14,14 @@ import { UserRole } from './common/enums';
* npm run seed * npm run seed
* *
* Environment variables (or defaults): * Environment variables (or defaults):
* ADMIN_EMAIL=admin@cloudhost.local * ADMIN_EMAIL=test@example.com
* ADMIN_PASSWORD=Admin123! * ADMIN_PASSWORD=Admin123!
*/ */
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.createApplicationContext(AppModule); const app = await NestFactory.createApplicationContext(AppModule);
const usersService = app.get(UsersService); 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 password = process.env.ADMIN_PASSWORD || 'Admin123!';
const existing = await usersService.findByEmail(email); const existing = await usersService.findByEmail(email);