|
| 1 | +# ============================================ |
| 2 | +# Stage 1: Builder - Clone pyatlan from GitHub |
| 3 | +# ============================================ |
| 4 | +FROM cgr.dev/atlan.com/pyatlan-golden:3.11 AS builder |
| 5 | + |
| 6 | +# Build arguments |
| 7 | +# PYATLAN_VERSION should be passed via --build-arg |
| 8 | +# Default to "latest" if not specified (workflow reads from version.txt) |
| 9 | +ARG PYATLAN_VERSION=latest |
| 10 | +ARG PYATLAN_COMMIT_SHA="" |
| 11 | + |
| 12 | +USER root |
| 13 | +WORKDIR /tmp/build |
| 14 | + |
| 15 | +# Clone pyatlan from GitHub |
| 16 | +RUN echo "=== Cloning pyatlan from GitHub ===" && \ |
| 17 | + git clone https://github.com/atlanhq/atlan-python.git /tmp/build/atlan-python |
| 18 | + |
| 19 | +# Checkout specific version or commit |
| 20 | +RUN cd /tmp/build/atlan-python && \ |
| 21 | + if [ -n "$PYATLAN_COMMIT_SHA" ]; then \ |
| 22 | + echo "Checking out commit: $PYATLAN_COMMIT_SHA" && \ |
| 23 | + git checkout "$PYATLAN_COMMIT_SHA"; \ |
| 24 | + elif [ "$PYATLAN_VERSION" = "latest" ]; then \ |
| 25 | + echo "Using latest from main branch"; \ |
| 26 | + else \ |
| 27 | + echo "Checking out version: v$PYATLAN_VERSION" && \ |
| 28 | + (git checkout "v$PYATLAN_VERSION" || git checkout "$PYATLAN_VERSION"); \ |
| 29 | + fi && \ |
| 30 | + echo "Commit: $(git rev-parse HEAD)" && \ |
| 31 | + echo "Version: $(cat pyatlan/version.txt)" |
| 32 | + |
| 33 | +# ============================================ |
| 34 | +# Stage 2: Final - Runtime image |
| 35 | +# ============================================ |
1 | 36 | FROM cgr.dev/atlan.com/pyatlan-golden:3.11 |
2 | 37 |
|
3 | | -# Build arguments for configurable versions |
| 38 | +# Build arguments for labels |
4 | 39 | ARG PYTHON_VERSION=3.11 |
5 | | -ARG PYATLAN_VERSION=latest |
6 | | -ARG PYATLAN_BRANCH="" |
7 | | -ARG INSTALL_FROM_GIT=false |
| 40 | +ARG PYATLAN_COMMIT_SHA="" |
8 | 41 |
|
| 42 | +# Copy version.txt from builder and set PYATLAN_VERSION if not provided |
| 43 | +COPY --from=builder /tmp/build/atlan-python/pyatlan/version.txt /tmp/version.txt |
| 44 | +ARG PYATLAN_VERSION |
| 45 | +RUN if [ -z "$PYATLAN_VERSION" ]; then \ |
| 46 | + PYATLAN_VERSION=$(cat /tmp/version.txt | tr -d '\n\r'); \ |
| 47 | + fi && \ |
| 48 | + echo "Using PYATLAN_VERSION: $PYATLAN_VERSION" && \ |
| 49 | + echo "$PYATLAN_VERSION" > /tmp/final_version.txt |
| 50 | + |
| 51 | +USER root |
9 | 52 | WORKDIR /app |
10 | 53 |
|
11 | 54 | # Set UV environment variables |
12 | | -# REMOVE THIS WHEN https://github.com/astral-sh/uv/issues/8635 IS FIXED |
13 | 55 | ENV UV_NO_MANAGED_PYTHON=true \ |
14 | 56 | UV_SYSTEM_PYTHON=true |
15 | 57 |
|
16 | | -# Switch to root for installation |
17 | | -USER root |
| 58 | +# Install pyatlan dependencies from Chainguard APK (hardened packages) |
| 59 | +RUN apk add --no-cache \ |
| 60 | + py3.11-pydantic=2.12.0-r0 \ |
| 61 | + py3.11-pydantic-core=2.41.1-r0 \ |
| 62 | + py3.11-jinja2=3.1.6-r1 \ |
| 63 | + py3.11-tenacity=9.1.2-r2 \ |
| 64 | + py3.11-pytz=2025.2-r2 \ |
| 65 | + py3.11-python-dateutil=2.9.0-r10 \ |
| 66 | + py3.11-pyyaml=6.0.3-r0 \ |
| 67 | + py3.11-httpx=0.28.1-r2 |
18 | 68 |
|
19 | | -# Install pyatlan - either from PyPI or git branch |
20 | | -RUN echo "Installing pyatlan==$PYATLAN_VERSION from PyPI"; \ |
21 | | - uv pip install --system pyatlan==$PYATLAN_VERSION; |
| 69 | +# Install dependencies and pyatlan in ONE layer (copy + install + cleanup) |
| 70 | +# This prevents Docker from keeping source files in image layers |
| 71 | +RUN --mount=type=bind,from=builder,source=/tmp/build/atlan-python,target=/mnt/source,readonly \ |
| 72 | + # Copy source to writable location |
| 73 | + cp -r /mnt/source /tmp/pyatlan-source && \ |
| 74 | + # Install non-APK dependencies |
| 75 | + uv pip install --system --no-cache \ |
| 76 | + lazy_loader~=0.4 \ |
| 77 | + nanoid~=2.0.0 \ |
| 78 | + httpx-retries~=0.4.0 && \ |
| 79 | + # Install pyatlan with --no-deps |
| 80 | + cd /tmp/pyatlan-source && \ |
| 81 | + uv pip install --system --no-cache --no-deps . && \ |
| 82 | + # Cleanup everything in same layer (source + caches + bytecode) |
| 83 | + cd / && rm -rf /tmp/pyatlan-source /root/.cache ~/.cache && \ |
| 84 | + rm -rf /tmp/version.txt /tmp/final_version.txt && \ |
| 85 | + find /usr/lib/python3.11 -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true && \ |
| 86 | + find /usr/lib/python3.11 -type f -name '*.pyc' -delete 2>/dev/null || true |
22 | 87 |
|
23 | | -# Add build information as labels |
24 | | -RUN if [ "$INSTALL_FROM_GIT" = "true" ]; then \ |
25 | | - echo "LABEL pyatlan.source=git" >> /tmp/labels && \ |
26 | | - echo "LABEL pyatlan.branch=$PYATLAN_BRANCH" >> /tmp/labels; \ |
27 | | - else \ |
28 | | - echo "LABEL pyatlan.source=pypi" >> /tmp/labels && \ |
29 | | - echo "LABEL pyatlan.version=$PYATLAN_VERSION" >> /tmp/labels; \ |
30 | | - fi && \ |
31 | | - echo "LABEL python.version=$PYTHON_VERSION" >> /tmp/labels |
| 88 | +# Verify installation and version |
| 89 | +RUN EXPECTED_VERSION="$(cat /tmp/final_version.txt)" python3 <<'EOF' |
| 90 | +import sys |
| 91 | +import os |
| 92 | +import pyatlan |
| 93 | +from pyatlan.client.atlan import AtlanClient |
| 94 | + |
| 95 | +expected_version = os.environ.get("EXPECTED_VERSION", "unknown") |
| 96 | + |
| 97 | +print("=== Pyatlan Installation Verification ===") |
| 98 | +print(f"Installed version: {pyatlan.__version__}") |
| 99 | +print(f"Expected version: {expected_version}") |
| 100 | + |
| 101 | +# Version validation (skip if 'latest' was requested) |
| 102 | +if expected_version != "latest": |
| 103 | + if pyatlan.__version__ != expected_version: |
| 104 | + print(f"❌ ERROR: Version mismatch!") |
| 105 | + print(f" Expected: {expected_version}") |
| 106 | + print(f" Got: {pyatlan.__version__}") |
| 107 | + sys.exit(1) |
| 108 | + |
| 109 | +print("✅ Version verified") |
| 110 | +print("✅ AtlanClient imported successfully") |
| 111 | +print("=== Build verification passed ===") |
| 112 | +EOF |
| 113 | + |
| 114 | +# Add build metadata as labels |
| 115 | +LABEL python.version="${PYTHON_VERSION}" |
| 116 | +LABEL pyatlan.commit_sha="${PYATLAN_COMMIT_SHA}" |
| 117 | +# Note: pyatlan.version label should be set by passing --build-arg PYATLAN_VERSION=$(cat pyatlan/version.txt) during build |
| 118 | +LABEL build.method="git-multistage" |
| 119 | +LABEL build.source="github" |
| 120 | +LABEL security.approach="apk-first-no-pypi" |
32 | 121 |
|
33 | | -# Switch back to nonroot user |
| 122 | +# Switch to nonroot user for runtime security |
34 | 123 | USER nonroot |
35 | 124 |
|
36 | | -# Default working directory for applications |
| 125 | +# Default working directory |
37 | 126 | WORKDIR /app |
38 | 127 |
|
39 | | -# Default command (can be overridden by extending images) |
| 128 | +# Default command |
40 | 129 | CMD ["python"] |
0 commit comments