Harden platform security, reliability, and CI after full audit.

Close deployment IDOR and gate stub payment endpoints, add production
secret validation, health probes, Redis-backed build progress, GitHub
Actions CI, expanded tests, billing/k8s refactors, and ops runbooks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
keyhan
2026-06-29 20:59:49 +03:30
parent a87bc49393
commit 837f0fa63f
83 changed files with 3953 additions and 1308 deletions
+55 -456
View File
@@ -1,469 +1,68 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { BuildService } from './build.service';
import { Application } from '../applications/entities/application.entity';
import { AppRuntime } from '../common/enums';
import { ClustersService } from '../clusters/clusters.service';
import { RegistryService } from '../kubernetes/registry.service';
import { BuildProgressStore } from './build-progress.store';
/**
* Tests for build service — Dockerfile generation for all runtimes
*/
describe('BuildService', () => {
let service: BuildService;
// ─────────────────────────────────────────────────────────────────────────────
// Go Dockerfile tests
// ─────────────────────────────────────────────────────────────────────────────
describe('Go Dockerfile generation', () => {
function goDockerfile(app: { runtimeVersion?: string; port?: number }): string {
const goVersion = app.runtimeVersion || '1.22';
const port = app.port || 8080;
return `FROM golang:${goVersion}-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git
COPY go.mod go.sum* ./
RUN go mod download || true
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o main .
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
BuildService,
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
const map: Record<string, string> = {
'build.namespace': 'cloudhost-builds',
'build.serviceAccount': 'kaniko-builder',
'registry.url': 'registry.local:5000',
};
return map[key];
}),
},
},
{ provide: ClustersService, useValue: {} },
{
provide: BuildProgressStore,
useValue: { get: jest.fn(), set: jest.fn(), clear: jest.fn() },
},
{ provide: RegistryService, useValue: {} },
],
}).compile();
FROM alpine:3.19
WORKDIR /app
RUN apk --no-cache add ca-certificates tzdata
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup
COPY --from=builder /app/main .
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
USER appuser
ENV PORT=${port}
EXPOSE ${port}
HEALTHCHECK --interval=30s --timeout=3s CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/health || exit 1
CMD ["./main"]
`;
}
it('should use correct Go version', () => {
const df = goDockerfile({ runtimeVersion: '1.21' });
expect(df).toContain('FROM golang:1.21-alpine');
service = module.get(BuildService);
});
it('should default to Go 1.22', () => {
const df = goDockerfile({});
expect(df).toContain('FROM golang:1.22-alpine');
});
describe('generateDockerfile', () => {
it('generates Go Dockerfile with requested runtime version', () => {
const app = {
runtime: AppRuntime.GO,
runtimeVersion: '1.22',
port: 8080,
} as Application;
it('should build static binary with CGO_ENABLED=0', () => {
const df = goDockerfile({});
expect(df).toContain('CGO_ENABLED=0');
});
const dockerfile = (service as any).generateDockerfile(app) as string;
it('should use multi-stage build for smaller image', () => {
const df = goDockerfile({});
expect(df).toContain('AS builder');
expect(df).toContain('FROM alpine:3.19');
});
expect(dockerfile).toContain('FROM golang:1.22-alpine');
expect(dockerfile).toContain('EXPOSE 8080');
});
it('should include health check', () => {
const df = goDockerfile({ port: 8080 });
expect(df).toContain('HEALTHCHECK');
expect(df).toContain('http://localhost:8080/health');
});
it('generates Node.js Dockerfile with default port', () => {
const app = {
runtime: AppRuntime.NODEJS,
runtimeVersion: '20',
} as Application;
it('should create data directory for persistent storage', () => {
const df = goDockerfile({});
expect(df).toContain('mkdir -p /app/data');
});
const dockerfile = (service as any).generateDockerfile(app) as string;
it('should run as non-root user', () => {
const df = goDockerfile({});
expect(df).toContain('USER appuser');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Python Dockerfile tests
// ─────────────────────────────────────────────────────────────────────────────
describe('Python Dockerfile generation', () => {
function pythonDockerfile(app: { runtimeVersion?: string; port?: number }): string {
const pythonVersion = app.runtimeVersion || '3.12';
const port = app.port || 8000;
return `FROM python:${pythonVersion}-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential libpq-dev
COPY requirements.txt* ./
RUN pip install --no-cache-dir --user -r requirements.txt 2>/dev/null || pip install --no-cache-dir --user flask gunicorn
FROM python:${pythonVersion}-slim
WORKDIR /app
RUN groupadd -g 1001 appgroup && useradd -r -u 1001 -g appgroup appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY . .
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
ENV PORT=${port}
EXPOSE ${port}
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:${port}/health || exit 1
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:${port}", "app:app"]
`;
}
it('should use correct Python version', () => {
const df = pythonDockerfile({ runtimeVersion: '3.11' });
expect(df).toContain('FROM python:3.11-slim');
});
it('should default to Python 3.12', () => {
const df = pythonDockerfile({});
expect(df).toContain('FROM python:3.12-slim');
});
it('should use multi-stage build', () => {
const df = pythonDockerfile({});
expect(df).toContain('AS builder');
});
it('should install from requirements.txt', () => {
const df = pythonDockerfile({});
expect(df).toContain('requirements.txt');
});
it('should include health check', () => {
const df = pythonDockerfile({ port: 8000 });
expect(df).toContain('HEALTHCHECK');
expect(df).toContain('http://localhost:8000/health');
});
it('should run as non-root user', () => {
const df = pythonDockerfile({});
expect(df).toContain('USER appuser');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Django Dockerfile tests
// ─────────────────────────────────────────────────────────────────────────────
describe('Django Dockerfile generation', () => {
function djangoDockerfile(app: { runtimeVersion?: string; port?: number }): string {
const pythonVersion = app.runtimeVersion || '3.12';
const port = app.port || 8000;
return `FROM python:${pythonVersion}-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y build-essential libpq-dev
COPY requirements.txt* ./
RUN pip install --no-cache-dir --user -r requirements.txt 2>/dev/null || pip install --no-cache-dir --user django gunicorn
FROM python:${pythonVersion}-slim
WORKDIR /app
COPY --from=builder /root/.local /home/appuser/.local
COPY . .
RUN mkdir -p /app/staticfiles /app/media /app/data
USER appuser
ENV PORT=${port}
ENV DJANGO_SETTINGS_MODULE=config.settings
EXPOSE ${port}
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:${port}/health/ || exit 1
CMD ["sh", "-c", "python manage.py migrate --noinput && gunicorn config.wsgi:application --bind 0.0.0.0:${port}"]
`;
}
it('should use correct Python version', () => {
const df = djangoDockerfile({ runtimeVersion: '3.10' });
expect(df).toContain('FROM python:3.10-slim');
});
it('should set DJANGO_SETTINGS_MODULE', () => {
const df = djangoDockerfile({});
expect(df).toContain('DJANGO_SETTINGS_MODULE');
});
it('should create staticfiles and media directories', () => {
const df = djangoDockerfile({});
expect(df).toContain('/app/staticfiles');
expect(df).toContain('/app/media');
});
it('should run migrations on startup', () => {
const df = djangoDockerfile({});
expect(df).toContain('migrate');
});
it('should use gunicorn for production', () => {
const df = djangoDockerfile({});
expect(df).toContain('gunicorn');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// .NET Dockerfile tests
// ─────────────────────────────────────────────────────────────────────────────
describe('.NET Dockerfile generation', () => {
function dotnetDockerfile(app: { runtimeVersion?: string; port?: number }): string {
const dotnetVersion = app.runtimeVersion || '8.0';
const port = app.port || 5000;
return `FROM mcr.microsoft.com/dotnet/sdk:${dotnetVersion} AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore || true
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:${dotnetVersion}
WORKDIR /app
COPY --from=build /app/publish .
RUN mkdir -p /app/data
USER appuser
ENV ASPNETCORE_URLS=http://+:${port}
ENV ASPNETCORE_ENVIRONMENT=Production
EXPOSE ${port}
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:${port}/health || exit 1
CMD ["dotnet", "app.dll"]
`;
}
it('should use correct .NET version', () => {
const df = dotnetDockerfile({ runtimeVersion: '7.0' });
expect(df).toContain('dotnet/sdk:7.0');
expect(df).toContain('dotnet/aspnet:7.0');
});
it('should default to .NET 8.0', () => {
const df = dotnetDockerfile({});
expect(df).toContain('dotnet/sdk:8.0');
});
it('should use multi-stage build', () => {
const df = dotnetDockerfile({});
expect(df).toContain('AS build');
expect(df).toContain('dotnet/aspnet');
});
it('should publish in Release mode', () => {
const df = dotnetDockerfile({});
expect(df).toContain('-c Release');
});
it('should set ASPNETCORE_ENVIRONMENT to Production', () => {
const df = dotnetDockerfile({});
expect(df).toContain('ASPNETCORE_ENVIRONMENT=Production');
});
it('should configure ASPNETCORE_URLS for correct port', () => {
const df = dotnetDockerfile({ port: 8080 });
expect(df).toContain('ASPNETCORE_URLS=http://+:8080');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// PHP Dockerfile tests
// ─────────────────────────────────────────────────────────────────────────────
describe('PHP Dockerfile generation', () => {
function phpDockerfile(app: { phpVersion?: string; port?: number }): string {
const phpVersion = app.phpVersion || '8.3';
const port = app.port || 80;
return `FROM php:${phpVersion}-fpm-alpine
RUN apk add --no-cache nginx supervisor curl
RUN docker-php-ext-install pdo pdo_mysql opcache
WORKDIR /var/www/html
COPY . .
RUN mkdir -p /var/www/html/uploads /var/www/html/data
RUN chown -R www-data:www-data /var/www/html
EXPOSE ${port}
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
`;
}
it('should use correct PHP version', () => {
const df = phpDockerfile({ phpVersion: '8.2' });
expect(df).toContain('FROM php:8.2-fpm-alpine');
});
it('should default to PHP 8.3', () => {
const df = phpDockerfile({});
expect(df).toContain('FROM php:8.3-fpm-alpine');
});
it('should use FPM with nginx via supervisord', () => {
const df = phpDockerfile({});
expect(df).toContain('supervisord');
expect(df).toContain('nginx');
});
it('should install common PHP extensions', () => {
const df = phpDockerfile({});
expect(df).toContain('pdo');
expect(df).toContain('opcache');
});
it('should create upload and data directories', () => {
const df = phpDockerfile({});
expect(df).toContain('/var/www/html/uploads');
expect(df).toContain('/var/www/html/data');
});
});
/**
* Tests for the WordPress build flow — specifically:
* 1. Helper pod PVC race condition (must wait for termination)
* 2. WordPress Dockerfile generation correctness
* 3. Entrypoint should use ENTRYPOINT not CMD to avoid double docker-entrypoint.sh execution
*/
describe('WordPress Dockerfile generation', () => {
// Reproduce the wordpressDockerfile logic from build.service.ts
function wordpressDockerfile(app: {
runtimeVersion?: string;
phpVersion?: string;
codePath?: string;
port?: number;
}): string {
const wpVersion = app.runtimeVersion || '6.7';
const phpVersion = app.phpVersion || '8.3';
const hasUploadedCode = !!app.codePath;
return `FROM wordpress:${wpVersion}-php${phpVersion}-apache
RUN docker-php-ext-install opcache
RUN a2enmod rewrite
RUN echo "upload_max_filesize = 64M\\npost_max_size = 64M\\nmax_execution_time = 300\\nmemory_limit = 256M" > /usr/local/etc/php/conf.d/uploads.ini
${hasUploadedCode ? `COPY . /tmp/user-content
RUN mkdir -p /usr/src/wordpress-user
ENTRYPOINT ["cloudhost-entrypoint.sh"]
CMD []` : `CMD ["apache2-foreground"]`}
EXPOSE 80
`;
}
it('should use ENTRYPOINT (not CMD) when user uploaded code', () => {
const df = wordpressDockerfile({ codePath: '/some/path/source.zip' });
expect(df).toContain('ENTRYPOINT ["cloudhost-entrypoint.sh"]');
expect(df).not.toContain('CMD ["cloudhost-entrypoint.sh"]');
});
it('should use CMD apache2-foreground for fresh install (no code)', () => {
const df = wordpressDockerfile({});
expect(df).toContain('CMD ["apache2-foreground"]');
expect(df).not.toContain('ENTRYPOINT');
});
it('should use correct WordPress and PHP versions', () => {
const df = wordpressDockerfile({ runtimeVersion: '6.4', phpVersion: '8.2' });
expect(df).toContain('FROM wordpress:6.4-php8.2-apache');
});
it('should default to WP 6.7 and PHP 8.3', () => {
const df = wordpressDockerfile({});
expect(df).toContain('FROM wordpress:6.7-php8.3-apache');
});
it('should COPY user content when codePath exists', () => {
const df = wordpressDockerfile({ codePath: '/tmp/source.zip' });
expect(df).toContain('COPY . /tmp/user-content');
});
it('should NOT copy user content for fresh install', () => {
const df = wordpressDockerfile({});
expect(df).not.toContain('COPY . /tmp/user-content');
});
});
describe('Helper pod PVC race condition', () => {
it('should wait for pod deletion (not just fire-and-forget)', () => {
// Simulate the fix: after deleteNamespacedPod, poll readNamespacedPod until 404
const deletionSteps = [
{ exists: true }, // pod still terminating
{ exists: true }, // still terminating
{ exists: false }, // gone (404)
];
let pollCount = 0;
let fullyTerminated = false;
for (const step of deletionSteps) {
pollCount++;
if (!step.exists) {
fullyTerminated = true;
break;
}
}
expect(fullyTerminated).toBe(true);
expect(pollCount).toBe(3);
});
it('should time out if pod never terminates', () => {
const maxPolls = 30; // e.g. 60s / 2s interval
let pollCount = 0;
let timedOut = false;
while (pollCount < maxPolls) {
pollCount++;
// Pod always exists (simulating stuck termination)
const exists = true;
if (!exists) break;
}
if (pollCount >= maxPolls) {
timedOut = true;
}
expect(timedOut).toBe(true);
});
});
describe('WordPress entrypoint script', () => {
const entrypointScript = `#!/bin/bash
set -e
# Merge user wp-content into PVC
if [ -d /usr/src/wordpress-user/wp-content ]; then
mkdir -p /var/www/html/wp-content
cp -a /usr/src/wordpress-user/wp-content/. /var/www/html/wp-content/
chown -R www-data:www-data /var/www/html/wp-content
fi
exec docker-entrypoint.sh apache2-foreground`;
it('should call docker-entrypoint.sh exactly once (via exec)', () => {
const matches = entrypointScript.match(/docker-entrypoint\.sh/g);
expect(matches).toHaveLength(1);
});
it('should use exec to replace process', () => {
expect(entrypointScript).toContain('exec docker-entrypoint.sh apache2-foreground');
});
it('should merge wp-content on every start when staged content exists', () => {
expect(entrypointScript).toContain('/usr/src/wordpress-user/wp-content');
expect(entrypointScript).not.toContain('.user-content-merged');
});
it('should not copy user wp-config.php (credentials come from env vars)', () => {
expect(entrypointScript).not.toContain('wp-config.php');
});
it('should set proper ownership after merging wp-content', () => {
expect(entrypointScript).toContain('chown -R www-data:www-data /var/www/html/wp-content');
});
});
describe('WordPress zip structure handling', () => {
// The unzip init container handles single-subfolder flattening
it('should flatten single subfolder (public_html/) to root', () => {
// Simulate: zip contains only public_html/
const extractedItems = ['public_html'];
const count = extractedItems.length;
const firstItem = extractedItems[0];
let flattenedToRoot = false;
if (count === 1 && firstItem === 'public_html') {
// cp -a /tmp/extract/public_html/. /workspace-out/source/
flattenedToRoot = true;
}
expect(flattenedToRoot).toBe(true);
});
it('should copy as-is when multiple items exist', () => {
// Simulate: zip contains multiple items at root
const extractedItems = ['wp-admin', 'wp-content', 'wp-includes', 'index.php'];
const count = extractedItems.length;
let copiedAsIs = false;
if (count !== 1) {
copiedAsIs = true;
}
expect(copiedAsIs).toBe(true);
expect(dockerfile).toContain('FROM node:20');
expect(dockerfile).toContain('EXPOSE 3000');
});
});
});