diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index f4734988f93..76c29614215 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -23,6 +23,7 @@ namespace Aspire.Hosting; public static class PythonAppResourceBuilderExtensions { private const string DefaultVirtualEnvFolder = ".venv"; + private const string DefaultPythonVersion = "3.13"; /// /// Adds a python application to the application model. @@ -432,135 +433,39 @@ private static IResourceBuilder AddPythonAppCore( c.WithDockerfileBuilder(resource.WorkingDirectory, context => { - if (!c.Resource.TryGetLastAnnotation(out var pythonEnvironmentAnnotation) || - !pythonEnvironmentAnnotation.Uv) - { - // Use the default Dockerfile if not using UV - return; - } - if (!context.Resource.TryGetLastAnnotation(out var entrypointAnnotation)) { // No entrypoint annotation found, cannot generate Dockerfile return; } - var pythonVersion = pythonEnvironmentAnnotation.Version ?? PythonVersionDetector.DetectVersion(appDirectory, pythonEnvironmentAnnotation.VirtualEnvironment!); + // Try to get Python environment annotation + context.Resource.TryGetLastAnnotation(out var pythonEnvironmentAnnotation); + // Detect Python version + var pythonVersion = pythonEnvironmentAnnotation?.Version; if (pythonVersion is null) { - // Could not detect Python version, skip Dockerfile generation - return; + var virtualEnvironment = pythonEnvironmentAnnotation?.VirtualEnvironment; + pythonVersion = PythonVersionDetector.DetectVersion(appDirectory, virtualEnvironment); } + // if we could not detect Python version, use the default + pythonVersion ??= DefaultPythonVersion; + var entrypointType = entrypointAnnotation.Type; var entrypoint = entrypointAnnotation.Entrypoint; + + // Check if using UV + var isUsingUv = pythonEnvironmentAnnotation?.Uv ?? false; - // Determine entry command for Dockerfile - string[] entryCommand = entrypointType switch + if (isUsingUv) { - EntrypointType.Script => ["python", entrypoint], - EntrypointType.Module => ["python", "-m", entrypoint], - EntrypointType.Executable => [entrypoint], - _ => throw new InvalidOperationException($"Unsupported entrypoint type: {entrypointType}") - }; - - // Check if uv.lock exists in the working directory - var uvLockPath = Path.Combine(resource.WorkingDirectory, "uv.lock"); - var hasUvLock = File.Exists(uvLockPath); - - // Get custom base images from annotation, if present - context.Resource.TryGetLastAnnotation(out var baseImageAnnotation); - var buildImage = baseImageAnnotation?.BuildImage ?? $"ghcr.io/astral-sh/uv:python{pythonVersion}-bookworm-slim"; - var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"python:{pythonVersion}-slim-bookworm"; - - var builderStage = context.Builder - .From(buildImage, "builder") - .EmptyLine() - .Comment("Enable bytecode compilation and copy mode for the virtual environment") - .Env("UV_COMPILE_BYTECODE", "1") - .Env("UV_LINK_MODE", "copy") - .EmptyLine() - .WorkDir("/app") - .EmptyLine(); - - if (hasUvLock) - { - // If uv.lock exists, use locked mode for reproducible builds - builderStage - .Comment("Install dependencies first for better layer caching") - .Comment("Uses BuildKit cache mounts to speed up repeated builds") - .RunWithMounts( - "uv sync --locked --no-install-project --no-dev", - "type=cache,target=/root/.cache/uv", - "type=bind,source=uv.lock,target=uv.lock", - "type=bind,source=pyproject.toml,target=pyproject.toml") - .EmptyLine() - .Comment("Copy the rest of the application source and install the project") - .Copy(".", "/app") - .RunWithMounts( - "uv sync --locked --no-dev", - "type=cache,target=/root/.cache/uv"); + GenerateUvDockerfile(context, resource, pythonVersion, entrypointType, entrypoint); } else { - // If uv.lock doesn't exist, copy pyproject.toml and generate lock file - builderStage - .Comment("Copy pyproject.toml to install dependencies") - .Copy("pyproject.toml", "/app/") - .EmptyLine() - .Comment("Install dependencies and generate lock file") - .Comment("Uses BuildKit cache mount to speed up repeated builds") - .RunWithMounts( - "uv sync --no-install-project --no-dev", - "type=cache,target=/root/.cache/uv") - .EmptyLine() - .Comment("Copy the rest of the application source and install the project") - .Copy(".", "/app") - .RunWithMounts( - "uv sync --no-dev", - "type=cache,target=/root/.cache/uv"); - } - - var runtimeBuilder = context.Builder - .From(runtimeImage, "app") - .EmptyLine() - .AddContainerFiles(context.Resource, "/app") - .Comment("------------------------------") - .Comment("🚀 Runtime stage") - .Comment("------------------------------") - .Comment("Create non-root user for security") - .Run("groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser") - .EmptyLine() - .Comment("Copy the application and virtual environment from builder") - .CopyFrom(builderStage.StageName!, "/app", "/app", "appuser:appuser") - .EmptyLine() - .Comment("Add virtual environment to PATH and set VIRTUAL_ENV") - .Env("PATH", "/app/.venv/bin:${PATH}") - .Env("VIRTUAL_ENV", "/app/.venv") - .Env("PYTHONDONTWRITEBYTECODE", "1") - .Env("PYTHONUNBUFFERED", "1") - .EmptyLine() - .Comment("Use the non-root user to run the application") - .User("appuser") - .EmptyLine() - .Comment("Set working directory") - .WorkDir("/app") - .EmptyLine() - .Comment("Run the application"); - - // Set the appropriate entrypoint and command based on entrypoint type - switch (entrypointType) - { - case EntrypointType.Script: - runtimeBuilder.Entrypoint(["python", entrypoint]); - break; - case EntrypointType.Module: - runtimeBuilder.Entrypoint(["python", "-m", entrypoint]); - break; - case EntrypointType.Executable: - runtimeBuilder.Entrypoint([entrypoint]); - break; + GenerateFallbackDockerfile(context, resource, pythonVersion, entrypointType, entrypoint); } }); }); @@ -581,6 +486,181 @@ private static IResourceBuilder AddPythonAppCore( return resourceBuilder; } + private static void GenerateUvDockerfile(DockerfileBuilderCallbackContext context, PythonAppResource resource, + string pythonVersion, EntrypointType entrypointType, string entrypoint) + { + // Check if uv.lock exists in the working directory + var uvLockPath = Path.Combine(resource.WorkingDirectory, "uv.lock"); + var hasUvLock = File.Exists(uvLockPath); + + // Get custom base images from annotation, if present + context.Resource.TryGetLastAnnotation(out var baseImageAnnotation); + var buildImage = baseImageAnnotation?.BuildImage ?? $"ghcr.io/astral-sh/uv:python{pythonVersion}-bookworm-slim"; + var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"python:{pythonVersion}-slim-bookworm"; + + var builderStage = context.Builder + .From(buildImage, "builder") + .EmptyLine() + .Comment("Enable bytecode compilation and copy mode for the virtual environment") + .Env("UV_COMPILE_BYTECODE", "1") + .Env("UV_LINK_MODE", "copy") + .EmptyLine() + .WorkDir("/app") + .EmptyLine(); + + if (hasUvLock) + { + // If uv.lock exists, use locked mode for reproducible builds + builderStage + .Comment("Install dependencies first for better layer caching") + .Comment("Uses BuildKit cache mounts to speed up repeated builds") + .RunWithMounts( + "uv sync --locked --no-install-project --no-dev", + "type=cache,target=/root/.cache/uv", + "type=bind,source=uv.lock,target=uv.lock", + "type=bind,source=pyproject.toml,target=pyproject.toml") + .EmptyLine() + .Comment("Copy the rest of the application source and install the project") + .Copy(".", "/app") + .RunWithMounts( + "uv sync --locked --no-dev", + "type=cache,target=/root/.cache/uv"); + } + else + { + // If uv.lock doesn't exist, copy pyproject.toml and generate lock file + builderStage + .Comment("Copy pyproject.toml to install dependencies") + .Copy("pyproject.toml", "/app/") + .EmptyLine() + .Comment("Install dependencies and generate lock file") + .Comment("Uses BuildKit cache mount to speed up repeated builds") + .RunWithMounts( + "uv sync --no-install-project --no-dev", + "type=cache,target=/root/.cache/uv") + .EmptyLine() + .Comment("Copy the rest of the application source and install the project") + .Copy(".", "/app") + .RunWithMounts( + "uv sync --no-dev", + "type=cache,target=/root/.cache/uv"); + } + + var runtimeBuilder = context.Builder + .From(runtimeImage, "app") + .EmptyLine() + .AddContainerFiles(context.Resource, "/app") + .Comment("------------------------------") + .Comment("🚀 Runtime stage") + .Comment("------------------------------") + .Comment("Create non-root user for security") + .Run("groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser") + .EmptyLine() + .Comment("Copy the application and virtual environment from builder") + .CopyFrom(builderStage.StageName!, "/app", "/app", "appuser:appuser") + .EmptyLine() + .Comment("Add virtual environment to PATH and set VIRTUAL_ENV") + .Env("PATH", "/app/.venv/bin:${PATH}") + .Env("VIRTUAL_ENV", "/app/.venv") + .Env("PYTHONDONTWRITEBYTECODE", "1") + .Env("PYTHONUNBUFFERED", "1") + .EmptyLine() + .Comment("Use the non-root user to run the application") + .User("appuser") + .EmptyLine() + .Comment("Set working directory") + .WorkDir("/app") + .EmptyLine() + .Comment("Run the application"); + + // Set the appropriate entrypoint and command based on entrypoint type + switch (entrypointType) + { + case EntrypointType.Script: + runtimeBuilder.Entrypoint(["python", entrypoint]); + break; + case EntrypointType.Module: + runtimeBuilder.Entrypoint(["python", "-m", entrypoint]); + break; + case EntrypointType.Executable: + runtimeBuilder.Entrypoint([entrypoint]); + break; + } + } + + private static void GenerateFallbackDockerfile(DockerfileBuilderCallbackContext context, PythonAppResource resource, + string pythonVersion, EntrypointType entrypointType, string entrypoint) + { + // Use the same runtime image as UV workflow for consistency + context.Resource.TryGetLastAnnotation(out var baseImageAnnotation); + var runtimeImage = baseImageAnnotation?.RuntimeImage ?? $"python:{pythonVersion}-slim-bookworm"; + + // Check if requirements.txt exists + var requirementsTxtPath = Path.Combine(resource.WorkingDirectory, "requirements.txt"); + var hasRequirementsTxt = File.Exists(requirementsTxtPath); + + var stage = context.Builder + .From(runtimeImage) + .EmptyLine() + .AddContainerFiles(context.Resource, "/app") + .Comment("------------------------------") + .Comment("🚀 Python Application") + .Comment("------------------------------") + .Comment("Create non-root user for security") + .Run("groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser") + .EmptyLine() + .Comment("Set working directory") + .WorkDir("/app") + .EmptyLine(); + + if (hasRequirementsTxt) + { + // Copy requirements.txt first for better layer caching + stage + .Comment("Copy requirements.txt for dependency installation") + .Copy("requirements.txt", "/app/requirements.txt") + .EmptyLine() + .Comment("Install dependencies using pip") + .Run( + """ + apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && pip install --no-cache-dir -r requirements.txt \ + && apt-get purge -y --auto-remove build-essential \ + && rm -rf /var/lib/apt/lists/* + """) + .EmptyLine(); + } + + // Copy the rest of the application + stage + .Comment("Copy application files") + .Copy(".", "/app", "appuser:appuser") + .EmptyLine() + .Comment("Set environment variables") + .Env("PYTHONDONTWRITEBYTECODE", "1") + .Env("PYTHONUNBUFFERED", "1") + .EmptyLine() + .Comment("Use the non-root user to run the application") + .User("appuser") + .EmptyLine() + .Comment("Run the application"); + + // Set the appropriate entrypoint based on entrypoint type + switch (entrypointType) + { + case EntrypointType.Script: + stage.Entrypoint(["python", entrypoint]); + break; + case EntrypointType.Module: + stage.Entrypoint(["python", "-m", entrypoint]); + break; + case EntrypointType.Executable: + stage.Entrypoint([entrypoint]); + break; + } + } + private static DockerfileStage AddContainerFiles(this DockerfileStage stage, IResource resource, string rootDestinationPath) { if (resource.TryGetAnnotationsOfType(out var containerFilesDestinationAnnotations)) diff --git a/src/Aspire.Hosting.Python/PythonVersionDetector.cs b/src/Aspire.Hosting.Python/PythonVersionDetector.cs index 2d9813a0b0a..d2f65b5e184 100644 --- a/src/Aspire.Hosting.Python/PythonVersionDetector.cs +++ b/src/Aspire.Hosting.Python/PythonVersionDetector.cs @@ -14,7 +14,7 @@ internal static partial class PythonVersionDetector /// The directory containing the Python application. /// The virtual environment to check as a fallback. /// The detected Python version in major.minor format (e.g., "3.13"), or null if not found. - public static string? DetectVersion(string appDirectory, VirtualEnvironment virtualEnvironment) + public static string? DetectVersion(string appDirectory, VirtualEnvironment? virtualEnvironment) { // First, try .python-version file (most specific) var pythonVersionFile = Path.Combine(appDirectory, ".python-version"); @@ -41,7 +41,12 @@ internal static partial class PythonVersionDetector } // Third, try detecting from virtual environment as ultimate fallback - return DetectVersionFromVirtualEnvironment(virtualEnvironment); + if (virtualEnvironment != null) + { + return DetectVersionFromVirtualEnvironment(virtualEnvironment); + } + + return null; } /// diff --git a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs index 422bdf45598..872315feae5 100644 --- a/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs +++ b/tests/Aspire.Hosting.Python.Tests/AddPythonAppTests.cs @@ -1372,5 +1372,157 @@ public async Task WithUvEnvironment_CustomBaseImages_GeneratesDockerfileWithCust // Verify the custom runtime image is used Assert.Contains("FROM python:3.13-slim AS app", dockerfileContent); } + + [Fact] + public async Task FallbackDockerfile_GeneratesDockerfileWithoutUv_WithRequirementsTxt() + { + using var sourceDir = new TempDirectory(); + using var outputDir = new TempDirectory(); + var projectDirectory = sourceDir.Path; + + // Create a Python project without UV but with requirements.txt + var requirementsContent = """ + flask==3.0.0 + requests==2.31.0 + """; + + var scriptContent = """ + print("Hello from non-UV project!") + """; + + File.WriteAllText(Path.Combine(projectDirectory, "requirements.txt"), requirementsContent); + File.WriteAllText(Path.Combine(projectDirectory, "main.py"), scriptContent); + + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputDir.Path, step: "publish-manifest"); + + // Add Python resources without UV environment + builder.AddPythonScript("script-app", projectDirectory, "main.py"); + + var app = builder.Build(); + app.Run(); + + // Verify that Dockerfile was generated + var dockerfilePath = Path.Combine(outputDir.Path, "script-app.Dockerfile"); + Assert.True(File.Exists(dockerfilePath), "Dockerfile should be generated for non-UV Python app"); + + var dockerfileContent = File.ReadAllText(dockerfilePath); + + // Verify it's a fallback Dockerfile (single stage, no UV) + Assert.DoesNotContain("uv sync", dockerfileContent); + Assert.DoesNotContain("ghcr.io/astral-sh/uv", dockerfileContent); + + // Verify it uses pip install for requirements.txt + Assert.Contains("pip install --no-cache-dir -r requirements.txt", dockerfileContent); + + // Verify it uses the same runtime image as UV workflow + Assert.Contains("FROM python:3.13-slim-bookworm", dockerfileContent); + + await Verify(dockerfileContent); + } + + [Fact] + public async Task FallbackDockerfile_GeneratesDockerfileWithoutUv_WithoutRequirementsTxt() + { + using var sourceDir = new TempDirectory(); + using var outputDir = new TempDirectory(); + var projectDirectory = sourceDir.Path; + + // Create a Python project without UV and without requirements.txt + var scriptContent = """ + print("Hello from non-UV project with no dependencies!") + """; + + var pyprojectContent = """ + [project] + name = "test-app" + version = "0.1.0" + requires-python = ">=3.11" + """; + + File.WriteAllText(Path.Combine(projectDirectory, "main.py"), scriptContent); + File.WriteAllText(Path.Combine(projectDirectory, "pyproject.toml"), pyprojectContent); + + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputDir.Path, step: "publish-manifest"); + + // Add Python resources without UV environment + builder.AddPythonScript("script-app", projectDirectory, "main.py"); + + var app = builder.Build(); + app.Run(); + + // Verify that Dockerfile was generated + var dockerfilePath = Path.Combine(outputDir.Path, "script-app.Dockerfile"); + Assert.True(File.Exists(dockerfilePath), "Dockerfile should be generated for non-UV Python app"); + + var dockerfileContent = File.ReadAllText(dockerfilePath); + + // Verify it's a fallback Dockerfile (single stage, no UV) + Assert.DoesNotContain("uv sync", dockerfileContent); + Assert.DoesNotContain("ghcr.io/astral-sh/uv", dockerfileContent); + + // Verify it doesn't have pip install since there's no requirements.txt + Assert.DoesNotContain("pip install", dockerfileContent); + + // Verify it uses the same runtime image as UV workflow + Assert.Contains("FROM python:3.11-slim-bookworm", dockerfileContent); + + await Verify(dockerfileContent); + } + + [Fact] + public async Task FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes() + { + using var sourceDir = new TempDirectory(); + using var outputDir = new TempDirectory(); + var projectDirectory = sourceDir.Path; + + // Create a Python project without UV + var scriptContent = """ + print("Hello!") + """; + + var pythonVersionContent = "3.12"; + + File.WriteAllText(Path.Combine(projectDirectory, "main.py"), scriptContent); + File.WriteAllText(Path.Combine(projectDirectory, ".python-version"), pythonVersionContent); + + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, outputDir.Path, step: "publish-manifest"); + + // Add Python resources with different entrypoint types, none using UV + builder.AddPythonScript("script-app", projectDirectory, "main.py"); + builder.AddPythonModule("module-app", projectDirectory, "mymodule"); + builder.AddPythonExecutable("executable-app", projectDirectory, "pytest"); + + var app = builder.Build(); + app.Run(); + + // Verify that Dockerfiles were generated for each entrypoint type + var scriptDockerfilePath = Path.Combine(outputDir.Path, "script-app.Dockerfile"); + Assert.True(File.Exists(scriptDockerfilePath), "Dockerfile should be generated for script entrypoint"); + + var moduleDockerfilePath = Path.Combine(outputDir.Path, "module-app.Dockerfile"); + Assert.True(File.Exists(moduleDockerfilePath), "Dockerfile should be generated for module entrypoint"); + + var executableDockerfilePath = Path.Combine(outputDir.Path, "executable-app.Dockerfile"); + Assert.True(File.Exists(executableDockerfilePath), "Dockerfile should be generated for executable entrypoint"); + + var scriptDockerfileContent = File.ReadAllText(scriptDockerfilePath); + var moduleDockerfileContent = File.ReadAllText(moduleDockerfilePath); + var executableDockerfileContent = File.ReadAllText(executableDockerfilePath); + + // Verify none use UV + Assert.DoesNotContain("uv sync", scriptDockerfileContent); + Assert.DoesNotContain("uv sync", moduleDockerfileContent); + Assert.DoesNotContain("uv sync", executableDockerfileContent); + + // Verify correct entrypoints + Assert.Contains("ENTRYPOINT [\"python\",\"main.py\"]", scriptDockerfileContent); + Assert.Contains("ENTRYPOINT [\"python\",\"-m\",\"mymodule\"]", moduleDockerfileContent); + Assert.Contains("ENTRYPOINT [\"pytest\"]", executableDockerfileContent); + + await Verify(scriptDockerfileContent) + .AppendContentAsFile(moduleDockerfileContent) + .AppendContentAsFile(executableDockerfileContent); + } } diff --git a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#00.verified.txt b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#00.verified.txt new file mode 100644 index 00000000000..40a1b033642 --- /dev/null +++ b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#00.verified.txt @@ -0,0 +1,23 @@ +FROM python:3.12-slim-bookworm + +# ------------------------------ +# 🚀 Python Application +# ------------------------------ +# Create non-root user for security +RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser + +# Set working directory +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appuser . /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Use the non-root user to run the application +USER appuser + +# Run the application +ENTRYPOINT ["python","main.py"] diff --git a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#01.verified.txt b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#01.verified.txt new file mode 100644 index 00000000000..9ebd703dec5 --- /dev/null +++ b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#01.verified.txt @@ -0,0 +1,23 @@ +FROM python:3.12-slim-bookworm + +# ------------------------------ +# 🚀 Python Application +# ------------------------------ +# Create non-root user for security +RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser + +# Set working directory +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appuser . /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Use the non-root user to run the application +USER appuser + +# Run the application +ENTRYPOINT ["python","-m","mymodule"] diff --git a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#02.verified.txt b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#02.verified.txt new file mode 100644 index 00000000000..3906ead4727 --- /dev/null +++ b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileForAllEntrypointTypes#02.verified.txt @@ -0,0 +1,23 @@ +FROM python:3.12-slim-bookworm + +# ------------------------------ +# 🚀 Python Application +# ------------------------------ +# Create non-root user for security +RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser + +# Set working directory +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appuser . /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Use the non-root user to run the application +USER appuser + +# Run the application +ENTRYPOINT ["pytest"] diff --git a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileWithoutUv_WithRequirementsTxt.verified.txt b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileWithoutUv_WithRequirementsTxt.verified.txt new file mode 100644 index 00000000000..722bcf8c45d --- /dev/null +++ b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileWithoutUv_WithRequirementsTxt.verified.txt @@ -0,0 +1,33 @@ +FROM python:3.13-slim-bookworm + +# ------------------------------ +# 🚀 Python Application +# ------------------------------ +# Create non-root user for security +RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser + +# Set working directory +WORKDIR /app + +# Copy requirements.txt for dependency installation +COPY requirements.txt /app/requirements.txt + +# Install dependencies using pip +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential \ + && pip install --no-cache-dir -r requirements.txt \ + && apt-get purge -y --auto-remove build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copy application files +COPY --chown=appuser:appuser . /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Use the non-root user to run the application +USER appuser + +# Run the application +ENTRYPOINT ["python","main.py"] diff --git a/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileWithoutUv_WithoutRequirementsTxt.verified.txt b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileWithoutUv_WithoutRequirementsTxt.verified.txt new file mode 100644 index 00000000000..54d167a23e1 --- /dev/null +++ b/tests/Aspire.Hosting.Python.Tests/Snapshots/AddPythonAppTests.FallbackDockerfile_GeneratesDockerfileWithoutUv_WithoutRequirementsTxt.verified.txt @@ -0,0 +1,23 @@ +FROM python:3.11-slim-bookworm + +# ------------------------------ +# 🚀 Python Application +# ------------------------------ +# Create non-root user for security +RUN groupadd --system --gid 999 appuser && useradd --system --gid 999 --uid 999 --create-home appuser + +# Set working directory +WORKDIR /app + +# Copy application files +COPY --chown=appuser:appuser . /app + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +# Use the non-root user to run the application +USER appuser + +# Run the application +ENTRYPOINT ["python","main.py"]