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:
@@ -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<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)`);
|
||||
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) {
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user