add optinal apps

This commit is contained in:
keyhan
2026-04-23 15:26:49 +03:30
parent f481a57d8f
commit 38748b0827
16 changed files with 3091 additions and 144 deletions
+293
View File
@@ -1,5 +1,298 @@
import { AppRuntime } from '../common/enums';
/**
* Tests for build service — Dockerfile generation for all runtimes
*/
// ─────────────────────────────────────────────────────────────────────────────
// 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 .
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');
});
it('should default to Go 1.22', () => {
const df = goDockerfile({});
expect(df).toContain('FROM golang:1.22-alpine');
});
it('should build static binary with CGO_ENABLED=0', () => {
const df = goDockerfile({});
expect(df).toContain('CGO_ENABLED=0');
});
it('should use multi-stage build for smaller image', () => {
const df = goDockerfile({});
expect(df).toContain('AS builder');
expect(df).toContain('FROM alpine:3.19');
});
it('should include health check', () => {
const df = goDockerfile({ port: 8080 });
expect(df).toContain('HEALTHCHECK');
expect(df).toContain('http://localhost:8080/health');
});
it('should create data directory for persistent storage', () => {
const df = goDockerfile({});
expect(df).toContain('mkdir -p /app/data');
});
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)
+290
View File
@@ -622,6 +622,16 @@ export class BuildService {
return this.laravelDockerfile(app);
case AppRuntime.WORDPRESS:
return this.wordpressDockerfile(app);
case AppRuntime.GO:
return this.goDockerfile(app);
case AppRuntime.PHP:
return this.phpDockerfile(app);
case AppRuntime.PYTHON:
return this.pythonDockerfile(app);
case AppRuntime.DJANGO:
return this.djangoDockerfile(app);
case AppRuntime.DOTNET:
return this.dotnetDockerfile(app);
default:
throw new Error(`Unsupported runtime: ${runtime}`);
}
@@ -874,6 +884,286 @@ CMD []` : `CMD ["apache2-foreground"]`}
`;
}
// ─── Go Dockerfile ─────────────────────────────────────────────────
private goDockerfile(app: Application): string {
const goVersion = app.runtimeVersion || '1.22';
const port = app.port || 8080;
return `# --- Build stage ---
FROM golang:${goVersion}-alpine AS builder
WORKDIR /app
# Install git for fetching dependencies
RUN apk add --no-cache git
# Copy go mod files first for better caching
COPY go.mod go.sum* ./
RUN go mod download || true
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o main .
# --- Production stage ---
FROM alpine:3.19
WORKDIR /app
# Add CA certificates for HTTPS requests
RUN apk --no-cache add ca-certificates tzdata
# Create non-root user
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001 -G appgroup
# Copy the binary from builder
COPY --from=builder /app/main .
COPY --from=builder /app/static ./static 2>/dev/null || true
COPY --from=builder /app/templates ./templates 2>/dev/null || true
COPY --from=builder /app/public ./public 2>/dev/null || true
# Create data directory for persistent storage
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
USER appuser
ENV PORT=${port}
EXPOSE ${port}
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD wget --no-verbose --tries=1 --spider http://localhost:${port}/health || exit 1
CMD ["./main"]
`;
}
// ─── PHP (Plain) Dockerfile ────────────────────────────────────────
private phpDockerfile(app: Application): 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 \\
&& docker-php-ext-install pdo pdo_mysql opcache \\
&& docker-php-ext-install pdo_pgsql 2>/dev/null || true
# Install common PHP extensions
RUN apk add --no-cache libpng-dev libjpeg-turbo-dev freetype-dev \\
&& docker-php-ext-configure gd --with-freetype --with-jpeg \\
&& docker-php-ext-install gd
WORKDIR /var/www/html
COPY . .
# Generate nginx config
RUN mkdir -p /etc/nginx/http.d && \\
echo 'server {' > /etc/nginx/http.d/default.conf && \\
echo ' listen ${port};' >> /etc/nginx/http.d/default.conf && \\
echo ' root /var/www/html;' >> /etc/nginx/http.d/default.conf && \\
echo ' index index.php index.html;' >> /etc/nginx/http.d/default.conf && \\
echo ' client_max_body_size 64M;' >> /etc/nginx/http.d/default.conf && \\
echo ' location / { try_files \\$uri \\$uri/ /index.php?\\$query_string; }' >> /etc/nginx/http.d/default.conf && \\
echo ' location ~ \\.php\\$ {' >> /etc/nginx/http.d/default.conf && \\
echo ' fastcgi_pass 127.0.0.1:9000;' >> /etc/nginx/http.d/default.conf && \\
echo ' fastcgi_param SCRIPT_FILENAME \\$document_root\\$fastcgi_script_name;' >> /etc/nginx/http.d/default.conf && \\
echo ' include fastcgi_params;' >> /etc/nginx/http.d/default.conf && \\
echo ' }' >> /etc/nginx/http.d/default.conf && \\
echo ' location ~ /\\.ht { deny all; }' >> /etc/nginx/http.d/default.conf && \\
echo '}' >> /etc/nginx/http.d/default.conf
# Generate supervisord config
RUN echo '[supervisord]' > /etc/supervisord.conf && \\
echo 'nodaemon=true' >> /etc/supervisord.conf && \\
echo 'logfile=/dev/stdout' >> /etc/supervisord.conf && \\
echo 'logfile_maxbytes=0' >> /etc/supervisord.conf && \\
echo '[program:php-fpm]' >> /etc/supervisord.conf && \\
echo 'command=php-fpm -F' >> /etc/supervisord.conf && \\
echo 'autostart=true' >> /etc/supervisord.conf && \\
echo 'autorestart=true' >> /etc/supervisord.conf && \\
echo 'stdout_logfile=/dev/stdout' >> /etc/supervisord.conf && \\
echo 'stdout_logfile_maxbytes=0' >> /etc/supervisord.conf && \\
echo 'stderr_logfile=/dev/stderr' >> /etc/supervisord.conf && \\
echo 'stderr_logfile_maxbytes=0' >> /etc/supervisord.conf && \\
echo '[program:nginx]' >> /etc/supervisord.conf && \\
echo 'command=nginx -g "daemon off;"' >> /etc/supervisord.conf && \\
echo 'autostart=true' >> /etc/supervisord.conf && \\
echo 'autorestart=true' >> /etc/supervisord.conf && \\
echo 'stdout_logfile=/dev/stdout' >> /etc/supervisord.conf && \\
echo 'stdout_logfile_maxbytes=0' >> /etc/supervisord.conf && \\
echo 'stderr_logfile=/dev/stderr' >> /etc/supervisord.conf && \\
echo 'stderr_logfile_maxbytes=0' >> /etc/supervisord.conf
# Use custom configs if provided
RUN [ -f docker/nginx.conf ] && cp docker/nginx.conf /etc/nginx/http.d/default.conf || true
RUN [ -f docker/supervisord.conf ] && cp docker/supervisord.conf /etc/supervisord.conf || true
# Create upload and data directories
RUN mkdir -p /var/www/html/uploads /var/www/html/data \\
&& chown -R www-data:www-data /var/www/html
EXPOSE ${port}
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
`;
}
// ─── Python Dockerfile ─────────────────────────────────────────────
private pythonDockerfile(app: Application): string {
const pythonVersion = app.runtimeVersion || '3.12';
const port = app.port || 8000;
return `# --- Build stage ---
FROM python:${pythonVersion}-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \\
build-essential libpq-dev \\
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install dependencies
COPY requirements.txt* ./
RUN pip install --no-cache-dir --user -r requirements.txt 2>/dev/null || \\
pip install --no-cache-dir --user flask gunicorn
# --- Production stage ---
FROM python:${pythonVersion}-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \\
libpq5 curl \\
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -g 1001 appgroup && useradd -r -u 1001 -g appgroup appuser
# Copy installed packages from builder
COPY --from=builder /root/.local /home/appuser/.local
# Copy application code
COPY . .
# Create data directory
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
ENV PORT=${port}
ENV PYTHONUNBUFFERED=1
EXPOSE ${port}
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"]
`;
}
// ─── Django Dockerfile ─────────────────────────────────────────────
private djangoDockerfile(app: Application): string {
const pythonVersion = app.runtimeVersion || '3.12';
const port = app.port || 8000;
return `# --- Build stage ---
FROM python:${pythonVersion}-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \\
build-essential libpq-dev \\
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install dependencies
COPY requirements.txt* ./
RUN pip install --no-cache-dir --user -r requirements.txt 2>/dev/null || \\
pip install --no-cache-dir --user django gunicorn psycopg2-binary mysqlclient
# --- Production stage ---
FROM python:${pythonVersion}-slim
WORKDIR /app
# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \\
libpq5 default-libmysqlclient-dev curl \\
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -g 1001 appgroup && useradd -r -u 1001 -g appgroup appuser
# Copy installed packages from builder
COPY --from=builder /root/.local /home/appuser/.local
# Copy application code
COPY . .
# Create directories for static files and media
RUN mkdir -p /app/staticfiles /app/media /app/data \\
&& chown -R appuser:appgroup /app
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
ENV PORT=${port}
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=config.settings
EXPOSE ${port}
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\\\" && \\
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 \\
"]
`;
}
// ─── .NET Dockerfile ───────────────────────────────────────────────
private dotnetDockerfile(app: Application): string {
const dotnetVersion = app.runtimeVersion || '8.0';
const port = app.port || 5000;
return `# --- Build stage ---
FROM mcr.microsoft.com/dotnet/sdk:${dotnetVersion} AS build
WORKDIR /src
# Copy csproj and restore dependencies
COPY *.csproj ./
RUN dotnet restore || true
# Copy everything else and build
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore 2>/dev/null || \\
dotnet publish -c Release -o /app/publish
# --- Production stage ---
FROM mcr.microsoft.com/dotnet/aspnet:${dotnetVersion}
WORKDIR /app
# Create non-root user
RUN groupadd -g 1001 appgroup && useradd -r -u 1001 -g appgroup appuser
# Copy published app
COPY --from=build /app/publish .
# Create data directory
RUN mkdir -p /app/data && chown -R appuser:appgroup /app
USER appuser
ENV ASPNETCORE_URLS=http://+:${port}
ENV DOTNET_RUNNING_IN_CONTAINER=true
ENV ASPNETCORE_ENVIRONMENT=Production
EXPOSE ${port}
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 entry point DLL
CMD ["sh", "-c", "DLL=$(find . -maxdepth 1 -name '*.dll' ! -name '*.deps.dll' ! -name '*.runtimeconfig.dll' | head -1) && dotnet $DLL"]
`;
}
private async waitForJobCompletion(
batchApi: k8s.BatchV1Api,
coreApi: k8s.CoreV1Api,