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);
|
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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user