revert(build): remove app build pipeline revamp (Nixpacks/MinIO/Trivy/registry GC)

Reverts commits 3eff38f and c379a23 and restores the previous Kaniko-only
build pipeline (runtime detection + per-runtime Dockerfile generation,
disk-based source upload).

Removed: Nixpacks Dockerfile generation, MinIO source storage (common/storage),
Bull build queue + Redis build state (common/redis, deployment.processor),
Trivy image scan (scan.service, deployment.vulnerabilitySummary), and daily
registry garbage collection (registry-gc). Nothing outside the build/deploy
path depended on these. Backend tsc + 105/106 tests green (the pre-existing
helm.service chartPath failure is unrelated); frontend tsc green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-23 19:21:30 +03:30
parent bd14eb2daa
commit 9c16b462f4
27 changed files with 1297 additions and 1951 deletions
+290 -80
View File
@@ -1,16 +1,298 @@
import { AppRuntime } from '../common/enums';
/**
* Tests for build service:
* • Nixpacks build preparation (BYO Dockerfile vs generated) for code runtimes
* • WordPress templated Dockerfile + helper-pod / entrypoint / zip-structure logic
*
* NOTE: like the rest of this file, the Nixpacks tests reproduce the pure logic
* locally instead of importing BuildService — the service pulls in the ESM
* `@kubernetes/client-node`, which this project's Jest config does not transform.
* Keep these copies in sync with nixpacksPrepareInitContainer in build.service.ts.
* 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)
@@ -185,75 +467,3 @@ describe('WordPress zip structure handling', () => {
expect(copiedAsIs).toBe(true);
});
});
describe('Nixpacks build preparation', () => {
// Local copies of the pure logic in build.service.ts (see NOTE at top of file).
function shellQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}
function nixpacksPlanEnv(app: { runtime: AppRuntime; runtimeVersion?: string }): { name: string; value: string }[] {
const env: { name: string; value: string }[] = [];
if (app.runtime === AppRuntime.NODEJS && app.runtimeVersion) {
env.push({ name: 'NIXPACKS_NODE_VERSION', value: String(app.runtimeVersion) });
}
if ((app.runtime === AppRuntime.PYTHON || app.runtime === AppRuntime.DJANGO) && app.runtimeVersion) {
env.push({ name: 'NIXPACKS_PYTHON_VERSION', value: String(app.runtimeVersion) });
}
return env;
}
function nixpacksPrepareInitContainer(
app: { runtime: AppRuntime; runtimeVersion?: string },
config: { nixpacksImage?: string; nixpacksBuildEnv?: string[] } = {},
): any {
const image = config.nixpacksImage || 'ghcr.io/railwayapp/nixpacks:latest';
const buildEnv = config.nixpacksBuildEnv || [];
const envFlags = buildEnv.map((kv) => `--env ${shellQuote(kv)}`).join(' ');
const planEnv = nixpacksPlanEnv(app);
return {
name: 'nixpacks-prepare',
image,
env: planEnv.length ? planEnv : undefined,
command: [
'sh',
'-c',
`if [ -f source/Dockerfile ]; then cp source/Dockerfile /workspace/Dockerfile; ` +
`else nixpacks build source --out source ${envFlags} && cp source/.nixpacks/Dockerfile /workspace/Dockerfile; fi`,
],
volumeMounts: [{ name: 'workspace', mountPath: '/workspace' }],
};
}
it('prefers a user-provided Dockerfile (BYO), falling back to Nixpacks', () => {
const script = nixpacksPrepareInitContainer({ runtime: AppRuntime.NODEJS }).command[2] as string;
expect(script).toContain('if [ -f source/Dockerfile ]');
expect(script).toContain('cp source/Dockerfile /workspace/Dockerfile');
expect(script).toContain('nixpacks build source --out source');
expect(script).toContain('cp source/.nixpacks/Dockerfile /workspace/Dockerfile');
});
it('uses the configured Nixpacks image (default when unset)', () => {
expect(nixpacksPrepareInitContainer({ runtime: AppRuntime.GO }).image).toBe('ghcr.io/railwayapp/nixpacks:latest');
expect(
nixpacksPrepareInitContainer({ runtime: AppRuntime.GO }, { nixpacksImage: 'registry.local/nixpacks:1.2.3' }).image,
).toBe('registry.local/nixpacks:1.2.3');
});
it('bakes build-time mirror env into the build via --env flags', () => {
const script = nixpacksPrepareInitContainer(
{ runtime: AppRuntime.NODEJS },
{ nixpacksBuildEnv: ['NPM_CONFIG_REGISTRY=https://registry.npmmirror.com'] },
).command[2] as string;
expect(script).toContain(`--env 'NPM_CONFIG_REGISTRY=https://registry.npmmirror.com'`);
});
it('maps the selected Node version to NIXPACKS_NODE_VERSION', () => {
const c = nixpacksPrepareInitContainer({ runtime: AppRuntime.NODEJS, runtimeVersion: '20' });
expect(c.env).toContainEqual({ name: 'NIXPACKS_NODE_VERSION', value: '20' });
});
it('shellQuote escapes embedded single quotes safely', () => {
expect(shellQuote("a'b")).toBe("'a'\\''b'");
});
});