|
1 | | -FROM cgr.dev/atlan.com/python:3.11 |
| 1 | +# ============================================ |
| 2 | +# Stage 1: Builder - Clone pyatlan from GitHub |
| 3 | +# ============================================ |
| 4 | +FROM cgr.dev/atlan.com/pyatlan-golden:3.11 AS builder |
2 | 5 |
|
3 | | -# Build arguments for configurable versions |
4 | | -ARG PYTHON_VERSION=3.11 |
| 6 | +# Build arguments |
| 7 | +# PYATLAN_VERSION should be passed via --build-arg |
| 8 | +# Default to "latest" if not specified (workflow reads from version.txt) |
5 | 9 | ARG PYATLAN_VERSION=latest |
6 | | -ARG PYATLAN_BRANCH="" |
7 | | -ARG INSTALL_FROM_GIT=false |
| 10 | +ARG PYATLAN_COMMIT_SHA="" |
8 | 11 |
|
9 | | -WORKDIR /app |
| 12 | +USER root |
| 13 | +WORKDIR /tmp/build |
| 14 | + |
| 15 | +# Install git and build tools |
| 16 | +RUN apk add --no-cache git py3.11-build |
10 | 17 |
|
11 | | -# Install pyatlan - either from PyPI or git branch |
12 | | -RUN --mount=type=cache,target=/root/.cache/uv \ |
13 | | - if [ "$INSTALL_FROM_GIT" = "true" ]; then \ |
14 | | - echo "Installing pyatlan from git branch: $PYATLAN_BRANCH"; \ |
15 | | - uv pip install --system "git+https://github.com/atlanhq/atlan-python.git@$PYATLAN_BRANCH"; \ |
| 18 | +# Clone pyatlan from GitHub |
| 19 | +RUN echo "=== Cloning pyatlan from GitHub ===" && \ |
| 20 | + git clone https://github.com/atlanhq/atlan-python.git /tmp/build/atlan-python |
| 21 | + |
| 22 | +# Checkout specific version or commit |
| 23 | +RUN cd /tmp/build/atlan-python && \ |
| 24 | + if [ -n "$PYATLAN_COMMIT_SHA" ]; then \ |
| 25 | + echo "Checking out commit: $PYATLAN_COMMIT_SHA" && \ |
| 26 | + git checkout "$PYATLAN_COMMIT_SHA"; \ |
16 | 27 | elif [ "$PYATLAN_VERSION" = "latest" ]; then \ |
17 | | - echo "Installing latest pyatlan from PyPI"; \ |
18 | | - uv pip install --system pyatlan; \ |
| 28 | + echo "Using latest from main branch"; \ |
19 | 29 | else \ |
20 | | - echo "Installing pyatlan==$PYATLAN_VERSION from PyPI"; \ |
21 | | - uv pip install --system pyatlan==$PYATLAN_VERSION; \ |
22 | | - fi |
23 | | - |
24 | | -# Add build information as labels |
25 | | -RUN if [ "$INSTALL_FROM_GIT" = "true" ]; then \ |
26 | | - echo "LABEL pyatlan.source=git" >> /tmp/labels && \ |
27 | | - echo "LABEL pyatlan.branch=$PYATLAN_BRANCH" >> /tmp/labels; \ |
| 30 | + echo "Checking out version: v$PYATLAN_VERSION" && \ |
| 31 | + (git checkout "v$PYATLAN_VERSION" || git checkout "$PYATLAN_VERSION"); \ |
| 32 | + fi && \ |
| 33 | + echo "Commit: $(git rev-parse HEAD)" && \ |
| 34 | + echo "Version: $(cat pyatlan/version.txt)" && \ |
| 35 | + # Build wheel in builder stage (smaller than copying full source) |
| 36 | + python3 -m build --wheel --outdir /tmp/wheels . && \ |
| 37 | + # Preserve version.txt before removing source |
| 38 | + cp pyatlan/version.txt /tmp/version.txt && \ |
| 39 | + # Remove everything except the wheel and version.txt |
| 40 | + cd /tmp && \ |
| 41 | + rm -rf /tmp/build/atlan-python && \ |
| 42 | + echo "Built wheel: $(ls -lh /tmp/wheels/*.whl)" && \ |
| 43 | + echo "Preserved version: $(cat /tmp/version.txt)" |
| 44 | + |
| 45 | +# ============================================ |
| 46 | +# Stage 2: Final - Runtime image |
| 47 | +# ============================================ |
| 48 | +FROM cgr.dev/atlan.com/pyatlan-golden:3.11 |
| 49 | + |
| 50 | +# Build arguments for labels |
| 51 | +ARG PYTHON_VERSION=3.11 |
| 52 | +ARG PYATLAN_COMMIT_SHA="" |
| 53 | + |
| 54 | +# Copy version.txt from builder and set PYATLAN_VERSION if not provided |
| 55 | +COPY --from=builder /tmp/version.txt /tmp/version.txt |
| 56 | +ARG PYATLAN_VERSION |
| 57 | +RUN echo "=== Debug: Version setup ===" && \ |
| 58 | + echo "Build arg PYATLAN_VERSION: '$PYATLAN_VERSION'" && \ |
| 59 | + echo "Contents of version.txt: '$(cat /tmp/version.txt)'" && \ |
| 60 | + if [ -z "$PYATLAN_VERSION" ]; then \ |
| 61 | + PYATLAN_VERSION=$(cat /tmp/version.txt | tr -d '\n\r'); \ |
| 62 | + echo "Using version from file: $PYATLAN_VERSION"; \ |
28 | 63 | else \ |
29 | | - echo "LABEL pyatlan.source=pypi" >> /tmp/labels && \ |
30 | | - echo "LABEL pyatlan.version=$PYATLAN_VERSION" >> /tmp/labels; \ |
| 64 | + echo "Using build arg version: $PYATLAN_VERSION"; \ |
31 | 65 | fi && \ |
32 | | - echo "LABEL python.version=$PYTHON_VERSION" >> /tmp/labels |
| 66 | + echo "Final PYATLAN_VERSION: $PYATLAN_VERSION" && \ |
| 67 | + echo "$PYATLAN_VERSION" > /tmp/final_version.txt && \ |
| 68 | + echo "Contents of final_version.txt: '$(cat /tmp/final_version.txt)'" |
| 69 | + |
| 70 | +USER root |
| 71 | +WORKDIR /app |
| 72 | + |
| 73 | +# Set UV environment variables |
| 74 | +ENV UV_NO_MANAGED_PYTHON=true \ |
| 75 | + UV_SYSTEM_PYTHON=true |
| 76 | + |
| 77 | +# Install pyatlan dependencies from Chainguard APK (hardened packages) |
| 78 | +RUN apk add --no-cache \ |
| 79 | + py3.11-pydantic=2.12.5-r0 \ |
| 80 | + py3.11-jinja2=3.1.6-r1 \ |
| 81 | + py3.11-tenacity=9.1.2-r2 \ |
| 82 | + py3.11-pytz=2025.2-r2 \ |
| 83 | + py3.11-python-dateutil=2.9.0-r10 \ |
| 84 | + py3.11-pyyaml=6.0.3-r0 \ |
| 85 | + py3.11-httpx=0.28.1-r2 |
| 86 | + |
| 87 | +# Copy only the wheel from builder (much smaller than full source) |
| 88 | +COPY --from=builder /tmp/wheels/*.whl /tmp/ |
| 89 | + |
| 90 | +# Install dependencies + pyatlan wheel + cleanup in ONE layer |
| 91 | +RUN uv pip install --system --no-cache \ |
| 92 | + lazy-loader~=0.4 \ |
| 93 | + nanoid~=2.0.0 \ |
| 94 | + authlib~=1.6.0 \ |
| 95 | + httpx-retries~=0.4.0 && \ |
| 96 | + # Install pyatlan wheel with --no-deps |
| 97 | + uv pip install --system --no-cache --no-deps /tmp/*.whl && \ |
| 98 | + # Cleanup everything EXCEPT version files (needed for verification) |
| 99 | + cd / && rm -rf /tmp/*.whl /root/.cache ~/.cache && \ |
| 100 | + find /usr/lib/python3.11 -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true && \ |
| 101 | + find /usr/lib/python3.11 -type f -name '*.pyc' -delete 2>/dev/null || true |
| 102 | + |
| 103 | +# Verify installation and version |
| 104 | +RUN echo "=== Debug: Checking version files ===" && \ |
| 105 | + ls -la /tmp/ && \ |
| 106 | + echo "Contents of final_version.txt:" && \ |
| 107 | + cat /tmp/final_version.txt && \ |
| 108 | + echo "=== Starting verification ===" && \ |
| 109 | + EXPECTED_VERSION="$(cat /tmp/final_version.txt)" python3 <<'EOF' |
| 110 | +import sys |
| 111 | +import os |
| 112 | + |
| 113 | +expected_version = os.environ.get("EXPECTED_VERSION", "unknown") |
| 114 | +print("=== Pyatlan Installation Verification ===") |
| 115 | +print(f"Expected version from env: {expected_version}") |
| 116 | + |
| 117 | +try: |
| 118 | + import pyatlan |
| 119 | + print(f"✅ PyAtlan imported successfully") |
| 120 | + print(f"Installed version: {pyatlan.__version__}") |
| 121 | + |
| 122 | + # Try importing AtlanClient |
| 123 | + from pyatlan.client.atlan import AtlanClient |
| 124 | + print(f"✅ AtlanClient imported successfully") |
| 125 | + |
| 126 | + # Version validation (skip if 'latest' was requested) |
| 127 | + if expected_version != "latest" and expected_version != "unknown": |
| 128 | + if pyatlan.__version__ != expected_version: |
| 129 | + print(f"❌ ERROR: Version mismatch!") |
| 130 | + print(f" Expected: {expected_version}") |
| 131 | + print(f" Got: {pyatlan.__version__}") |
| 132 | + sys.exit(1) |
| 133 | + else: |
| 134 | + print("✅ Version verified") |
| 135 | + else: |
| 136 | + print("✅ Version validation skipped (latest or unknown)") |
| 137 | + |
| 138 | + print("=== Build verification passed ===") |
| 139 | + |
| 140 | +except ImportError as e: |
| 141 | + print(f"❌ Import error: {e}") |
| 142 | + sys.exit(1) |
| 143 | +except Exception as e: |
| 144 | + print(f"❌ Unexpected error: {e}") |
| 145 | + sys.exit(1) |
| 146 | +EOF |
| 147 | + |
| 148 | +# Final cleanup of temporary version files |
| 149 | +RUN rm -rf /tmp/version.txt /tmp/final_version.txt |
| 150 | + |
| 151 | +# Add build metadata as labels |
| 152 | +LABEL python.version="${PYTHON_VERSION}" |
| 153 | +LABEL pyatlan.commit_sha="${PYATLAN_COMMIT_SHA}" |
| 154 | +# Note: pyatlan.version label should be set by passing --build-arg PYATLAN_VERSION=$(cat pyatlan/version.txt) during build |
| 155 | +LABEL build.method="git-multistage" |
| 156 | +LABEL build.source="github" |
| 157 | +LABEL security.approach="apk-first-no-pypi" |
| 158 | + |
| 159 | +# Switch to nonroot user for runtime security |
| 160 | +USER nonroot |
33 | 161 |
|
34 | | -# Default working directory for applications |
| 162 | +# Default working directory |
35 | 163 | WORKDIR /app |
36 | 164 |
|
37 | | -# Default command (can be overridden by extending images) |
| 165 | +# Default command |
38 | 166 | CMD ["python"] |
0 commit comments