diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fde3a6b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +# Default-deny: ignore everything, then re-include only what the build needs. +# The Dockerfile currently uses no COPY/ADD, so the build context can be empty. +* + +# Keep Dockerfile itself accessible to buildx. +!Dockerfile diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a1f4e4a..bd744c7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -29,35 +29,24 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Read Versions - id: versions - run: | - echo "kubectl=$(grep '^kubectl=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "gcloud=$(grep '^gcloud=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "aws_cli=$(grep '^aws_cli=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "azure_cli=$(grep '^azure_cli=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "scalr_cli=$(grep '^scalr_cli=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "python=$(grep '^python=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "python_release=$(grep '^python_release=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - - - name: Build Docker image - uses: docker/build-push-action@v6 + # Build all variants single-arch so they can be loaded locally and smoke-tested. + - name: Build images + uses: docker/bake-action@v5 with: - build-args: | - KUBECTL_VERSION=${{ steps.versions.outputs.kubectl }} - GCLOUD_VERSION=${{ steps.versions.outputs.gcloud }} - AWS_CLI_VERSION=${{ steps.versions.outputs.aws_cli }} - AZURE_CLI_VERSION=${{ steps.versions.outputs.azure_cli }} - SCALR_CLI_VERSION=${{ steps.versions.outputs.scalr_cli }} - PYTHON_VERSION=${{ steps.versions.outputs.python }} - PYTHON_RELEASE=${{ steps.versions.outputs.python_release }} - cache-from: type=registry,ref=scalr/runner:buildcache - cache-to: type=registry,ref=scalr/runner:buildcache + files: | + docker-bake.hcl + versions.json load: true - tags: | - scalr/runner:sha-${{ github.sha }} + set: | + *.platform=linux/amd64 + full.tags=scalr/runner:sha-${{ github.sha }} + python39.tags=scalr/runner:sha-${{ github.sha }}-python39 + slim.tags=scalr/runner:sha-${{ github.sha }}-slim + full.cache-to=type=registry,ref=scalr/runner:buildcache,mode=max + python39.cache-to=type=registry,ref=scalr/runner:buildcache-python39,mode=max + slim.cache-to=type=registry,ref=scalr/runner:buildcache-slim,mode=max - - name: Test Docker Image + - name: Test full image run: | docker run --rm scalr/runner:sha-${{ github.sha }} -xc 'gcloud version' docker run --rm scalr/runner:sha-${{ github.sha }} -xc 'aws --version' @@ -68,25 +57,17 @@ jobs: docker run --rm scalr/runner:sha-${{ github.sha }} -xc 'pip --version' docker run --rm scalr/runner:sha-${{ github.sha }} -xc 'pip install requests' - - name: Build Docker image (Python 3.9) - uses: docker/build-push-action@v6 - with: - build-args: | - KUBECTL_VERSION=${{ steps.versions.outputs.kubectl }} - GCLOUD_VERSION=${{ steps.versions.outputs.gcloud }} - AWS_CLI_VERSION=${{ steps.versions.outputs.aws_cli }} - AZURE_CLI_VERSION=${{ steps.versions.outputs.azure_cli }} - SCALR_CLI_VERSION=${{ steps.versions.outputs.scalr_cli }} - PYTHON_VERSION=3.9.25 - PYTHON_RELEASE=20251031 - cache-from: type=registry,ref=scalr/runner:buildcache-python39 - cache-to: type=registry,ref=scalr/runner:buildcache-python39 - load: true - tags: | - scalr/runner:sha-${{ github.sha }}-python39 - - - name: Test Docker Image (Python 3.9) + - name: Test python39 image run: | docker run --rm scalr/runner:sha-${{ github.sha }}-python39 -xc 'python --version' docker run --rm scalr/runner:sha-${{ github.sha }}-python39 -xc 'pip --version' docker run --rm scalr/runner:sha-${{ github.sha }}-python39 -xc 'pip install requests' + + - name: Test slim image + run: | + docker run --rm scalr/runner:sha-${{ github.sha }}-slim -xc 'git --version' + docker run --rm scalr/runner:sha-${{ github.sha }}-slim -xc 'curl --version' + docker run --rm scalr/runner:sha-${{ github.sha }}-slim -xc 'jq --version' + # Confirm python and cloud tools are absent. + docker run --rm scalr/runner:sha-${{ github.sha }}-slim -xc '! command -v python' + docker run --rm scalr/runner:sha-${{ github.sha }}-slim -xc '! command -v aws' diff --git a/.github/workflows/build_and_release_gar.yaml b/.github/workflows/build_and_release_gar.yaml new file mode 100644 index 0000000..d1a7186 --- /dev/null +++ b/.github/workflows/build_and_release_gar.yaml @@ -0,0 +1,131 @@ +name: Build and Release Runner Image (EU dev GAR) + +# Triggered when a PR carries the `build-gar-images` label. Pushes test +# builds of all three variants to the EU dev GAR mirror only — Docker +# Hub and the US production mirror are never touched here (production +# is reserved for the release.yaml workflow). +# +# Tags are derived from the PR source branch, lower-cased and prefixed +# with `branch-` so they cannot collide with the semver release tags +# produced by release.yaml. Example: branch `0.2.0` → `branch-0.2.0`, +# not `0.2.0`. This prevents a PR from a maliciously-named branch from +# overwriting an existing release image. + +on: + pull_request: + types: [labeled, synchronize, reopened] + +permissions: + contents: read + # Required by google-github-actions/auth for workload identity federation. + id-token: write + +jobs: + build: + name: Build and Push to GAR (EU dev) + if: contains(github.event.pull_request.labels.*.name, 'build-gar-images') + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Defense-in-depth: refuse to build on behalf of senders without + # write-or-better access on this repo. Covers two paths: + # - `labeled` event: the user who added the label must be trusted. + # - `synchronize` event: the user who pushed must be trusted. + # GitHub already strips secrets/vars for `pull_request` runs from + # forks, so this is layered on top of that built-in protection. + - name: Verify sender authorization + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACTOR: ${{ github.event.sender.login }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + role=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" --jq .permission) + case "$role" in + admin|maintain|write) echo "OK: ${ACTOR} has ${role} on ${REPO}";; + *) echo "::error::Sender ${ACTOR} has '${role}' permission; needs write or higher"; exit 1;; + esac + + - name: Authenticate to Google Cloud + id: gcp-auth + uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{vars.GOOGLE_WORKLOAD_IDENTITY_POOL_PROVIDER}} + service_account: ${{vars.GOOGLE_SERVICE_ACCOUNT_EMAIL}} + token_format: access_token + + - name: Login to GAR (EU dev) + uses: docker/login-action@v3 + with: + registry: ${{ vars.EU_DEV_MIRROR_LOCATION }}-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.gcp-auth.outputs.access_token }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # PR source branch → Docker tag. Lower-cased (downstream agents + # lower-case image refs), slashes → dashes (illegal in tags), and + # prefixed with `branch-` so the tag can never collide with a + # semver release tag pushed by release.yaml. + - name: Resolve branch tag + id: branch + run: | + raw='${{ github.head_ref }}' + sanitized="${raw,,}" + sanitized="${sanitized//\//-}" + echo "tag=branch-${sanitized}" | tee -a $GITHUB_OUTPUT + + - name: Compose GAR image path + id: gar + run: | + echo "image=${{ vars.EU_DEV_MIRROR_LOCATION }}-docker.pkg.dev/${{ vars.EU_DEV_GOOGLE_PROJECT }}/main/scalr/runner" | tee -a $GITHUB_OUTPUT + + - name: Build and push images + uses: docker/bake-action@v5 + env: + VERSION: ${{ steps.branch.outputs.tag }} + with: + files: | + docker-bake.hcl + versions.json + push: true + # Replace each target's tag list (`tags=` removes the Docker Hub + # default from docker-bake.hcl) so the build pushes only to the EU + # dev GAR mirror, which also holds the per-branch buildcache. + set: | + full.tags=${{ steps.gar.outputs.image }}:${{ steps.branch.outputs.tag }} + python39.tags=${{ steps.gar.outputs.image }}:${{ steps.branch.outputs.tag }}-python39 + slim.tags=${{ steps.gar.outputs.image }}:${{ steps.branch.outputs.tag }}-slim + full.cache-from=type=registry,ref=${{ steps.gar.outputs.image }}:buildcache-${{ steps.branch.outputs.tag }} + python39.cache-from=type=registry,ref=${{ steps.gar.outputs.image }}:buildcache-${{ steps.branch.outputs.tag }}-python39 + slim.cache-from=type=registry,ref=${{ steps.gar.outputs.image }}:buildcache-${{ steps.branch.outputs.tag }}-slim + full.cache-to=type=registry,ref=${{ steps.gar.outputs.image }}:buildcache-${{ steps.branch.outputs.tag }},mode=max + python39.cache-to=type=registry,ref=${{ steps.gar.outputs.image }}:buildcache-${{ steps.branch.outputs.tag }}-python39,mode=max + slim.cache-to=type=registry,ref=${{ steps.gar.outputs.image }}:buildcache-${{ steps.branch.outputs.tag }}-slim,mode=max + + - name: Report published images + env: + BRANCH: ${{ github.head_ref }} + TAG: ${{ steps.branch.outputs.tag }} + IMG: ${{ steps.gar.outputs.image }} + run: | + echo "Published GAR (EU dev) images for branch ${BRANCH}" + echo "" + echo " ${IMG}:${TAG}" + echo " ${IMG}:${TAG}-python39" + echo " ${IMG}:${TAG}-slim" + { + echo "## Published runner images — branch \`${BRANCH}\`" + echo "" + echo "**GAR — EU dev**" + echo "" + echo "- \`${IMG}:${TAG}\`" + echo "- \`${IMG}:${TAG}-python39\`" + echo "- \`${IMG}:${TAG}-slim\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 97734a1..4ce1f76 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,6 +5,12 @@ on: tags: - "*.*.*" +permissions: + # Needed by the update_changelog job to git-push CHANGELOG.md back to main. + contents: write + # Required by google-github-actions/auth for workload identity federation. + id-token: write + jobs: build: name: Build and Push @@ -19,65 +25,115 @@ jobs: username: ${{ vars.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} + # Authenticate to Google Artifact Registry (GAR) so the same build can be + # mirrored to the internal Scalr registry alongside Docker Hub. + - name: Authenticate to Google Cloud + id: gcp-auth + uses: google-github-actions/auth@v3 + with: + workload_identity_provider: ${{vars.GOOGLE_WORKLOAD_IDENTITY_POOL_PROVIDER}} + service_account: ${{vars.GOOGLE_SERVICE_ACCOUNT_EMAIL}} + token_format: access_token + + - name: Login to GAR (EU dev) + uses: docker/login-action@v3 + with: + registry: ${{ vars.EU_DEV_MIRROR_LOCATION }}-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.gcp-auth.outputs.access_token }} + + - name: Login to GAR (US prod) + uses: docker/login-action@v3 + with: + registry: ${{ vars.US_PROD_MIRROR_LOCATION }}-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.gcp-auth.outputs.access_token }} + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Read Versions - id: versions - run: | - echo "kubectl=$(grep '^kubectl=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "gcloud=$(grep '^gcloud=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "aws_cli=$(grep '^aws_cli=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "azure_cli=$(grep '^azure_cli=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "scalr_cli=$(grep '^scalr_cli=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "python=$(grep '^python=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - echo "python_release=$(grep '^python_release=' versions | cut -d= -f2)" | tee -a $GITHUB_OUTPUT - - name: Format Image Tag id: image_tag run: | echo "tag=${GITHUB_REF#refs/tags/}" | tee -a $GITHUB_OUTPUT - - name: Build Docker image - uses: docker/build-push-action@v6 - with: - build-args: | - KUBECTL_VERSION=${{ steps.versions.outputs.kubectl }} - GCLOUD_VERSION=${{ steps.versions.outputs.gcloud }} - AWS_CLI_VERSION=${{ steps.versions.outputs.aws_cli }} - AZURE_CLI_VERSION=${{ steps.versions.outputs.azure_cli }} - SCALR_CLI_VERSION=${{ steps.versions.outputs.scalr_cli }} - PYTHON_VERSION=${{ steps.versions.outputs.python }} - PYTHON_RELEASE=${{ steps.versions.outputs.python_release }} - platforms: linux/amd64,linux/arm64 - cache-from: type=registry,ref=scalr/runner:buildcache - cache-to: type=registry,ref=scalr/runner:buildcache - push: true - tags: | - scalr/runner:latest - scalr/runner:${{ steps.image_tag.outputs.tag }} + # Two regional GAR mirrors — EU dev and US production. + - name: Compose GAR image paths + id: gar + run: | + echo "image_eu=${{ vars.EU_DEV_MIRROR_LOCATION }}-docker.pkg.dev/${{ vars.EU_DEV_GOOGLE_PROJECT }}/main/scalr/runner" | tee -a $GITHUB_OUTPUT + echo "image_us=${{ vars.US_PROD_MIRROR_LOCATION }}-docker.pkg.dev/${{ vars.US_PROD_GOOGLE_PROJECT }}/main/scalr/runner" | tee -a $GITHUB_OUTPUT - - name: Build Docker image (Python 3.9) - uses: docker/build-push-action@v6 + - name: Build and push images + uses: docker/bake-action@v5 + env: + VERSION: ${{ steps.image_tag.outputs.tag }} with: - build-args: | - KUBECTL_VERSION=${{ steps.versions.outputs.kubectl }} - GCLOUD_VERSION=${{ steps.versions.outputs.gcloud }} - AWS_CLI_VERSION=${{ steps.versions.outputs.aws_cli }} - AZURE_CLI_VERSION=${{ steps.versions.outputs.azure_cli }} - SCALR_CLI_VERSION=${{ steps.versions.outputs.scalr_cli }} - PYTHON_VERSION=3.9.25 - PYTHON_RELEASE=20251031 - platforms: linux/amd64,linux/arm64 - cache-from: type=registry,ref=scalr/runner:buildcache-python39 - cache-to: type=registry,ref=scalr/runner:buildcache-python39 + files: | + docker-bake.hcl + versions.json push: true - tags: | - scalr/runner:latest-python39 - scalr/runner:${{ steps.image_tag.outputs.tag }}-python39 + # Append both GAR region tags to each target so a single push writes + # to Docker Hub (declared in docker-bake.hcl) and both regional GAR + # mirrors. Cache-to is injected here because the docker-container + # driver supports it, while the bake file stays local-driver-friendly. + set: | + full.tags+=${{ steps.gar.outputs.image_eu }}:${{ steps.image_tag.outputs.tag }} + full.tags+=${{ steps.gar.outputs.image_us }}:${{ steps.image_tag.outputs.tag }} + python39.tags+=${{ steps.gar.outputs.image_eu }}:${{ steps.image_tag.outputs.tag }}-python39 + python39.tags+=${{ steps.gar.outputs.image_us }}:${{ steps.image_tag.outputs.tag }}-python39 + slim.tags+=${{ steps.gar.outputs.image_eu }}:${{ steps.image_tag.outputs.tag }}-slim + slim.tags+=${{ steps.gar.outputs.image_us }}:${{ steps.image_tag.outputs.tag }}-slim + full.cache-to=type=registry,ref=scalr/runner:buildcache,mode=max + python39.cache-to=type=registry,ref=scalr/runner:buildcache-python39,mode=max + slim.cache-to=type=registry,ref=scalr/runner:buildcache-slim,mode=max + + - name: Report published images + env: + TAG: ${{ steps.image_tag.outputs.tag }} + IMG_EU: ${{ steps.gar.outputs.image_eu }} + IMG_US: ${{ steps.gar.outputs.image_us }} + run: | + echo "Published runner images for release ${TAG}" + echo "" + echo "Docker Hub:" + echo " scalr/runner:${TAG}" + echo " scalr/runner:${TAG}-python39" + echo " scalr/runner:${TAG}-slim" + echo "" + echo "GAR — EU dev:" + echo " ${IMG_EU}:${TAG}" + echo " ${IMG_EU}:${TAG}-python39" + echo " ${IMG_EU}:${TAG}-slim" + echo "" + echo "GAR — US prod:" + echo " ${IMG_US}:${TAG}" + echo " ${IMG_US}:${TAG}-python39" + echo " ${IMG_US}:${TAG}-slim" + { + echo "## Published runner images — \`${TAG}\`" + echo "" + echo "**Docker Hub**" + echo "" + echo "- \`scalr/runner:${TAG}\`" + echo "- \`scalr/runner:${TAG}-python39\`" + echo "- \`scalr/runner:${TAG}-slim\`" + echo "" + echo "**GAR — EU dev**" + echo "" + echo "- \`${IMG_EU}:${TAG}\`" + echo "- \`${IMG_EU}:${TAG}-python39\`" + echo "- \`${IMG_EU}:${TAG}-slim\`" + echo "" + echo "**GAR — US prod**" + echo "" + echo "- \`${IMG_US}:${TAG}\`" + echo "- \`${IMG_US}:${TAG}-python39\`" + echo "- \`${IMG_US}:${TAG}-slim\`" + } >> "$GITHUB_STEP_SUMMARY" update_changelog: name: Update Changelog diff --git a/Dockerfile b/Dockerfile index 5b38dc9..444f0fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,22 @@ +# check=skip=InvalidDefaultArgInFrom # Runner Image for the Scalr remote backend # -------------------------------------------- # # Note: This is a PUBLIC image, it should not contain any sensitive data. +# +# Build targets: +# - base: basic tools only (internal stage, not published) +# - slim: base + security hardening (published as scalr/runner:-slim) +# - full: base + Python + cloud CLIs + hardening (published as scalr/runner: +# and scalr/runner:-python39) +# +# DEBIAN_BASE_IMAGE, DEBIAN_BASE_DIGEST and other ARGs are supplied at build +# time via bake (see docker-bake.hcl + versions.json). The skip directive +# above silences BuildKit's check for ARGs in FROM without a default. -FROM debian:trixie-slim +ARG DEBIAN_BASE_IMAGE +ARG DEBIAN_BASE_DIGEST +FROM ${DEBIAN_BASE_IMAGE}@${DEBIAN_BASE_DIGEST} AS base ARG TARGETARCH @@ -20,20 +33,59 @@ RUN </dev/null || true +EOT + +ENTRYPOINT ["/usr/bin/bash"] + + +# ---------------------------------------------------------------------------- +# full: base + Python + cloud CLIs + hardening (final image) +# ---------------------------------------------------------------------------- +FROM base AS full + # Install python standalone build. ARG PYTHON_VERSION ARG PYTHON_RELEASE +ARG PYTHON_SHA256_AMD64 +ARG PYTHON_SHA256_ARM64 LABEL python.version=${PYTHON_VERSION} ENV PIP_ROOT_USER_ACTION=ignore @@ -41,13 +93,14 @@ RUN </dev/null || true +EOT ENTRYPOINT ["/usr/bin/bash"] diff --git a/README.md b/README.md index 05db1ba..badc1a9 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,139 @@ -# Runner Image used in Scalr Remote Backend +# Scalr Runner Image -This is the Git repo of the official runner image. +This repository hosts the build pipeline for the runner images used by the OpenTofu/Terraform +remote operations backend of the Scalr platform and [on-prem Scalr Agents](https://docs.scalr.io/docs/run-environment#runner-image). -The image is based on the [`debian:trixie-slim`](https://hub.docker.com/_/debian). +## Contents + +- [Image Variants](#image-variants) +- [Included Tools](#included-tools) + - [Base Software](#base-software) + - [Added in the full image](#added-in-the-full-image) + - [Added in the `-python39` image](#added-in-the--python39-image) + - [Runtime user](#runtime-user) +- [Building the Image](#building-the-image) +- [Bumping Versions](#bumping-versions) + +## Image Variants + +Three variants are published from this repository: + +- `scalr/runner:-slim` — slim image: a minimal set of tools. +- `scalr/runner:` — full image: the slim set plus cloud CLIs (AWS, Azure, gcloud, kubectl, scalr-cli) and Python 3.14. +- `scalr/runner:-python39` — same as the full image, but with Python 3.9 instead of 3.14 (for legacy workflows). + +Images are based on +[`debian:trixie-slim`](https://hub.docker.com/_/debian) (pinned by digest in +[versions.json](./versions.json)) and published as multi-arch manifests for +`linux/amd64` and `linux/arm64`. ## Included Tools -This environment comes pre-equipped with a comprehensive suite of tools essential for development, operations, and cloud interactions. Here's a breakdown of what's included: +The images provide a set of tools commonly used in IaC workflows, grouped +below by the variant they appear in. + +### Base Software + +Present in all variants. These come from the pinned Debian Trixie snapshot +referenced by `DEBIAN_BASE_DIGEST` in [versions.json](./versions.json), so +their exact versions are whatever that snapshot pins. -* **Archivators**: - * zip - Create and extract ZIP archives - * tar - Manipulate tar archives - * gzip - Compress and decompress `.gz` files +* **Archive tools**: + * `tar` — manipulate tar archives (from the base image) + * `gzip` — compress and decompress `.gz` files (from the base image) + * `zip`, `unzip` — create and extract ZIP archives * **Encryption**: - * gnupg - Secure data encryption and signing -* **Git (v2.47.2)**: - * Core Git functionality - * Git LFS (Large File Storage) - * SSH and HTTP transport protocols -* **HTTP Clients**: - * curl - Data transfer with URLs - * wget - File downloads from the web - * ca-certificates - Trusted CA certificates -* **Programming Languages** - * Python ([v3.14.4](https://www.python.org/downloads/release/python-3144/)) - General-purpose programming language (release) - * jq - Command-line JSON processor -* **Cloud Clients** - * AWS CLI ([2.34.29](https://github.com/aws/aws-cli/releases/tag/2.34.29)) - Amazon Web Services CLI. - * Azure CLI ([2.85.0](https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.85.0)) - Microsoft Azure CLI. - * Google Cloud SDK ([564.0.0](https://cloud.google.com/sdk/docs/release-notes#56400)) - Stable, Alpha, Beta components. Includes kubectl authenticator. - * Kubectl ([0.35.3](https://github.com/kubernetes/kubectl/releases/tag/v0.35.3)) - Kubernetes CLI. - * Scalr CLI ([0.17.8](https://github.com/Scalr/scalr-cli/releases/tag/v0.17.8)) - The command-line to communicate with the Scalr API. - -The versions for Python, Cloud Clients, Kubectl, and Scalr CLI are specifically pinned and detailed in the [versions](./versions) file. All other software included in this environment is sourced directly from the Debian Trixie upstream repositories. - -## Python Distribution - -The environment uses the [standalone Python build](https://github.com/astral-sh/python-build-standalone) provided by the [astral.sh](https://astral.sh/) team. - -Two Python variants are available: - -| Image Tag | Python Version | -|-----------|----------------| -| `scalr/runner:` | Python 3.14.x | -| `scalr/runner:-python39` | Python 3.9.x | - -## Runner Image Building + * `gnupg` — secure data encryption and signing +* **Git**: + * `git-core` — core Git + * `git-lfs` — Large File Storage extension + * `openssh-client` — SSH transport for Git over SSH +* **HTTP / network**: + * `curl` — data transfer with URLs + * `wget` — file downloads from the web + * `ca-certificates` — trusted CA bundle +* **System / misc**: + * `jq` — command-line JSON processor + * `lsb-release` — Linux Standard Base release info + * `bash` (default shell / entrypoint) + +### Added in the full image + +These are pinned by exact version + SHA256 in +[versions.json](./versions.json) and downloaded during the build. The +versions below are the current pins (kept in sync with `versions.json` by +`bump-versions.py`): + +* **Programming language** + * Python ([v3.14.5](https://www.python.org/downloads/release/python-3145/)) — [standalone CPython build](https://github.com/astral-sh/python-build-standalone) from [astral.sh](https://astral.sh/) +* **Cloud CLIs** + * AWS CLI ([2.34.53](https://github.com/aws/aws-cli/releases/tag/2.34.53)) — Amazon Web Services CLI + * AWS Session Manager Plugin — SSM session support for the AWS CLI + * Azure CLI ([2.86.0](https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.86.0)) — Microsoft Azure CLI + * Google Cloud SDK ([569.0.0](https://cloud.google.com/sdk/docs/release-notes#56900)) — `gcloud` with `alpha`, `beta`, and `gke-gcloud-auth-plugin` components + * Kubectl ([0.36.1](https://github.com/kubernetes/kubectl/releases/tag/v0.36.1)) — Kubernetes CLI + * Scalr CLI ([0.18.0](https://github.com/Scalr/scalr-cli/releases/tag/v0.18.0)) — command-line client for the Scalr API + +### Added in the `-python39` image + +Same as the full image, with Python 3.14 replaced by Python 3.9 (currently +[v3.9.25](https://www.python.org/downloads/release/python-3925/)). + +**Google Cloud SDK pin.** Newer `gcloud` releases dropped Python 3.9 +support, so the SDK is pinned to `564.0.0` (the last version that still +works on Python 3.9) via the `versions_python39` map. `bump-versions.py` +does not auto-bump this pin — if you ever change it, recompute the +per-arch SHA256s for the new version by hand. The full (Python 3.14) +image continues to track the latest gcloud release. + +### Runtime user + +A non-root user `scalr` with uid/gid `1000` is created in the base layer +and is therefore present in all variants. + +## Building the Image + +Builds are driven by [`docker-bake.hcl`](./docker-bake.hcl) (targets, tags, +cache config) and [`versions.json`](./versions.json) (pinned tool versions +and SHA256 checksums). `versions.json` is a native Docker Buildx Bake +variable file containing three maps: + +- `versions_base` — Debian base image and digest (used by every target, including `-slim`) +- `versions_full` — extra tools layered on top for the full image (kubectl, gcloud, AWS CLI, Azure CLI, Scalr CLI, Python 3.14, AWS SSM Plugin) +- `versions_python39` — Python 3.9 overrides merged on top of `versions_full` for the `-python39` image + +Always pass both files. Every download is verified by SHA256 in the +Dockerfile. + +Tags use `VERSION` from the environment, defaulting to `dev` for local +builds. + +The bake file declares `platforms = ["linux/amd64", "linux/arm64"]` for CI +multi-arch builds. Local builds with Docker's default driver cannot do +multi-platform, so add `--set "*.platform=linux/amd64"` (or your host arch) +and `--load` to every local command. + +### Build everything ```bash -docker buildx build \ - --build-arg PYTHON_VERSION=3.14.4 \ - --build-arg PYTHON_RELEASE=20260408 \ - --build-arg KUBECTL_VERSION=v1.35.3 \ - --build-arg GCLOUD_VERSION=564.0.0 \ - --build-arg AWS_CLI_VERSION=2.34.29 \ - --build-arg AZURE_CLI_VERSION=2.85.0 \ - --build-arg SCALR_CLI_VERSION=0.17.8 \ - --platform linux/amd64 \ - -t scalr/runner:latest --load . +VERSION=dev docker buildx bake -f docker-bake.hcl -f versions.json \ + --set "*.platform=linux/amd64" --load ``` -To build the Python 3.9 variant: +### Build one variant ```bash -docker buildx build \ - --build-arg PYTHON_VERSION=3.9.25 \ - --build-arg PYTHON_RELEASE=20251031 \ - --build-arg KUBECTL_VERSION=v1.35.3 \ - --build-arg GCLOUD_VERSION=564.0.0 \ - --build-arg AWS_CLI_VERSION=2.34.29 \ - --build-arg AZURE_CLI_VERSION=2.85.0 \ - --build-arg SCALR_CLI_VERSION=0.17.8 \ - --platform linux/amd64 \ - -t scalr/runner:latest-python39 --load . +# scalr/runner:dev +VERSION=dev docker buildx bake -f docker-bake.hcl -f versions.json \ + --set "*.platform=linux/amd64" --load full + +# scalr/runner:dev-python39 +VERSION=dev docker buildx bake -f docker-bake.hcl -f versions.json \ + --set "*.platform=linux/amd64" --load python39 + +# scalr/runner:dev-slim +VERSION=dev docker buildx bake -f docker-bake.hcl -f versions.json \ + --set "*.platform=linux/amd64" --load slim ``` ## Bumping Versions @@ -80,9 +141,22 @@ docker buildx build \ To update all tool versions to their latest releases, run: ```bash -./bump-versions.sh +./bump-versions.py ``` -This script fetches the latest versions from upstream sources and updates the [versions](./versions) file and README.md. +This script fetches the latest versions from upstream sources and updates +the `versions_base`, `versions_full`, and `versions_python39` maps in +[versions.json](./versions.json) (plus the [Included Tools](#included-tools) +section of this README). For every tool it also refreshes the per-arch +SHA256 checksums used by the Dockerfile to verify each download. + +Requirements: `python3` (stdlib only, no `pip install` needed). -Requirements: `curl` and `jq`. +GitHub's anonymous API quota is 60 requests/hour. The script makes ~5 calls +to `api.github.com` per run, so frequent reruns may hit `HTTP 403: rate +limit exceeded`. Export `GITHUB_TOKEN` (or `GH_TOKEN`) to lift the limit to +5000/hour: + +```bash +GITHUB_TOKEN=$(gh auth token) ./bump-versions.py +``` diff --git a/bump-versions.py b/bump-versions.py new file mode 100755 index 0000000..3ab7eb7 --- /dev/null +++ b/bump-versions.py @@ -0,0 +1,531 @@ +#!/usr/bin/env python3 +"""Bump tool versions in ./versions.json and refresh pinned SHA256 checksums. + +For every tool that has a discoverable upstream version index, this script +fetches the latest release, updates the VERSIONS / VERSIONS_PY39 maps in +versions.json (a Docker Buildx Bake variable file), and refreshes the +per-arch SHA256 checksums the Dockerfile uses to verify each download. The +AWS Session Manager Plugin version stays manually pinned (no upstream +version index) but its SHAs are still refreshed against the pinned version. + +Stdlib only — no pip dependencies. +""" + +from __future__ import annotations + +import hashlib +import json +import logging +import os +import re +import sys +import urllib.error +import urllib.request +from pathlib import Path +from typing import Callable + +VERSIONS_FILE = Path("versions.json") +README_FILE = Path("README.md") + +# Top-level variable names in versions.json: +# versions.json -> variable.
.default. = +SECTION_BASE = "versions_base" +SECTION_FULL = "versions_full" +SECTION_PY39 = "versions_python39" +# Default section for the bump() helper — tool-version bumps live in `full`. +SECTION_DEFAULT = SECTION_FULL +UA = "bump-versions.py (https://github.com/Scalr/runner)" +# Optional auth — GitHub's unauthenticated API quota is 60/hour per IP; +# 5000/hour with any valid token. CI sets GITHUB_TOKEN automatically. +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +log = logging.getLogger("bump-versions") + + +# --- HTTP / hash helpers ---------------------------------------------------- + + +def _request(url: str) -> urllib.request.Request: + headers = {"User-Agent": UA} + if GITHUB_TOKEN and "api.github.com" in url: + headers["Authorization"] = f"Bearer {GITHUB_TOKEN}" + return urllib.request.Request(url, headers=headers) + + +def http_get_text(url: str) -> str: + with urllib.request.urlopen(_request(url), timeout=60) as resp: + return resp.read().decode() + + +def http_get_json(url: str): + return json.loads(http_get_text(url)) + + +def compute_sha256_url(url: str) -> str: + """Stream URL through SHA256 without holding the whole file in memory.""" + h = hashlib.sha256() + with urllib.request.urlopen(_request(url), timeout=300) as resp: + for chunk in iter(lambda: resp.read(1 << 20), b""): + h.update(chunk) + return h.hexdigest() + + +def fetch_text_sha(url: str) -> str: + """Sidecar .sha256 file (single hash, optional trailing filename).""" + return http_get_text(url).strip().split()[0] + + +def fetch_sha_from_sumsfile(sums_url: str, asset: str) -> str: + """SHA256SUMS-style file: ' ' lines.""" + for line in http_get_text(sums_url).splitlines(): + parts = line.split() + if len(parts) >= 2 and parts[1] == asset: + return parts[0] + raise RuntimeError(f"{asset} not found in {sums_url}") + + +# --- versions.json I/O ------------------------------------------------------ + + +def _load() -> dict: + return json.loads(VERSIONS_FILE.read_text()) + + +def _dump(data: dict) -> None: + VERSIONS_FILE.write_text(json.dumps(data, indent=2) + "\n") + + +def read_versions(section: str = SECTION_DEFAULT) -> dict[str, str]: + try: + return dict(_load()["variable"][section]["default"]) + except (FileNotFoundError, KeyError): + return {} + + +def write_value(key: str, value: str, section: str = SECTION_DEFAULT) -> None: + """Update or append KEY in the given top-level variable and rewrite the file.""" + data = _load() + data.setdefault("variable", {}).setdefault(section, {}).setdefault("default", {})[key] = value + _dump(data) + + +# --- README pretty-section updaters ----------------------------------------- + + +def _readme_sub(pattern: str, replacement: str) -> None: + content = README_FILE.read_text() + new, n = re.subn(pattern, replacement, content, count=1) + if n == 0: + log.warning(f"README: no match for /{pattern}/") + return + README_FILE.write_text(new) + + +def update_readme_python(version: str) -> None: + no_dots = version.replace(".", "") + _readme_sub( + r"Python \(\[v[0-9.]+\]\(https://www\.python\.org/downloads/release/python-[0-9]+/\)\)", + f"Python ([v{version}](https://www.python.org/downloads/release/python-{no_dots}/))", + ) + + +def update_readme_aws_cli(version: str) -> None: + _readme_sub( + r"AWS CLI \(\[[0-9.]+\]\(https://github\.com/aws/aws-cli/releases/tag/[0-9.]+\)\)", + f"AWS CLI ([{version}](https://github.com/aws/aws-cli/releases/tag/{version}))", + ) + + +def update_readme_azure_cli(version: str) -> None: + _readme_sub( + r"Azure CLI \(\[[0-9.]+\]\(https://github\.com/Azure/azure-cli/releases/tag/azure-cli-[0-9.]+\)\)", + f"Azure CLI ([{version}](https://github.com/Azure/azure-cli/releases/tag/azure-cli-{version}))", + ) + + +def update_readme_gcloud(version: str) -> None: + no_dots = version.replace(".", "") + _readme_sub( + r"Google Cloud SDK \(\[[0-9.]+\]\(https://cloud\.google\.com/sdk/docs/release-notes#[^)]+\)\)", + f"Google Cloud SDK ([{version}](https://cloud.google.com/sdk/docs/release-notes#{no_dots}))", + ) + + +def update_readme_kubectl(version: str) -> None: + # version is "v1.36.1"; kubectl repo uses "0.36.1" matching k8s minor. + repo_ver = version.replace("v1.", "0.", 1) + _readme_sub( + r"Kubectl \(\[[0-9.]+\]\(https://github\.com/kubernetes/kubectl/releases/tag/v[0-9.]+\)\)", + f"Kubectl ([{repo_ver}](https://github.com/kubernetes/kubectl/releases/tag/v{repo_ver}))", + ) + + +def update_readme_scalr_cli(version: str) -> None: + _readme_sub( + r"Scalr CLI \(\[[0-9.]+\]\(https://github\.com/Scalr/scalr-cli/releases/tag/v[0-9.]+\)\)", + f"Scalr CLI ([{version}](https://github.com/Scalr/scalr-cli/releases/tag/v{version}))", + ) + + +# --- upstream version fetchers ---------------------------------------------- + + +def get_latest_kubectl() -> str: + return http_get_text("https://dl.k8s.io/release/stable.txt").strip() + + +def get_latest_gcloud() -> str: + html = http_get_text("https://cloud.google.com/sdk/docs/release-notes") + m = re.search(r"(\d+\.\d+\.\d+)", html) + return m.group(1) if m else "" + + +def get_latest_aws_cli() -> str: + tags = http_get_json("https://api.github.com/repos/aws/aws-cli/tags") + for t in tags: + if t["name"].startswith("2."): + return t["name"] + return "" + + +def get_latest_azure_cli() -> str: + rel = http_get_json("https://api.github.com/repos/Azure/azure-cli/releases/latest") + return rel["tag_name"].removeprefix("azure-cli-") + + +def get_latest_scalr_cli() -> str: + rel = http_get_json("https://api.github.com/repos/Scalr/scalr-cli/releases/latest") + return rel["tag_name"].removeprefix("v") + + +def _latest_python_version(series: str) -> tuple[str, str]: + """Latest (version, release) for the given series (e.g. '3.14' or '3.9'). + + Walks recent releases newest-first — python-build-standalone ships + different CPython series per release, and older series (e.g. 3.9) appear + only in some releases, so the "latest" release may not carry every series. + """ + # per_page>10 routinely 504s here — the response is large because each + # release lists ~100 assets. 10 is plenty: 3.9 appears in most releases. + releases = http_get_json( + "https://api.github.com/repos/astral-sh/python-build-standalone/releases?per_page=10" + ) + pat = re.compile(rf"cpython-({re.escape(series)}\.\d+)") + for rel in releases: + for asset in rel.get("assets", []): + m = pat.search(asset["name"]) + if m: + return m.group(1), rel["tag_name"] + return "", "" + + +def get_debian_base_digest(image_ref: str) -> str: + """Resolve a Docker Hub image reference (e.g. "debian:trixie-slim") to + its current manifest digest. Uses Docker Hub's anonymous v2 registry API. + """ + name, _, tag = image_ref.partition(":") + repo = name if "/" in name else f"library/{name}" + tag = tag or "latest" + token = http_get_json( + f"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{repo}:pull" + )["token"] + req = urllib.request.Request( + f"https://registry-1.docker.io/v2/{repo}/manifests/{tag}", + method="HEAD", + headers={ + "Authorization": f"Bearer {token}", + # Accept both multi-arch index formats — debian:trixie-slim returns the OCI form. + "Accept": ( + "application/vnd.docker.distribution.manifest.list.v2+json," + "application/vnd.oci.image.index.v1+json" + ), + }, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + return resp.headers["Docker-Content-Digest"] + + +def get_latest_python_info() -> tuple[str, str]: + """Return (version, release), e.g. ('3.14.5', '20260510').""" + return _latest_python_version("3.14") + + +def get_latest_python39_info() -> tuple[str, str]: + """Return (version, release) for the latest 3.9.x in python-build-standalone.""" + return _latest_python_version("3.9") + + +# --- per-tool SHA refresh --------------------------------------------------- + + +def refresh_kubectl_shas(version: str) -> None: + write_value( + "KUBECTL_SHA256_AMD64", + fetch_text_sha( + f"https://dl.k8s.io/release/{version}/bin/linux/amd64/kubectl.sha256" + ), + ) + write_value( + "KUBECTL_SHA256_ARM64", + fetch_text_sha( + f"https://dl.k8s.io/release/{version}/bin/linux/arm64/kubectl.sha256" + ), + ) + + +def refresh_python_shas(version: str, release: str, section: str = SECTION_DEFAULT) -> None: + sums = ( + "https://github.com/astral-sh/python-build-standalone/releases/download/" + f"{release}/SHA256SUMS" + ) + write_value( + "PYTHON_SHA256_AMD64", + fetch_sha_from_sumsfile( + sums, + f"cpython-{version}+{release}-x86_64-unknown-linux-gnu-pgo+lto-full.tar.zst", + ), + section, + ) + write_value( + "PYTHON_SHA256_ARM64", + fetch_sha_from_sumsfile( + sums, + f"cpython-{version}+{release}-aarch64-unknown-linux-gnu-pgo+lto-full.tar.zst", + ), + section, + ) + + +def refresh_gcloud_shas(version: str) -> None: + base = ( + "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/" + f"google-cloud-sdk-{version}-linux-" + ) + write_value("GCLOUD_SHA256_AMD64", compute_sha256_url(f"{base}x86_64.tar.gz")) + write_value("GCLOUD_SHA256_ARM64", compute_sha256_url(f"{base}arm.tar.gz")) + + +def refresh_aws_cli_shas(version: str) -> None: + base = "https://awscli.amazonaws.com/awscli-exe-linux-" + write_value( + "AWS_CLI_SHA256_AMD64", compute_sha256_url(f"{base}x86_64-{version}.zip") + ) + write_value( + "AWS_CLI_SHA256_ARM64", + compute_sha256_url(f"{base}aarch64-{version}.zip"), + ) + + +def refresh_scalr_cli_shas(version: str) -> None: + sums = ( + f"https://github.com/Scalr/scalr-cli/releases/download/v{version}/" + f"scalr-cli_{version}_SHA256SUMS" + ) + write_value( + "SCALR_CLI_SHA256_AMD64", + fetch_sha_from_sumsfile(sums, f"scalr-cli_{version}_linux_amd64.zip"), + ) + write_value( + "SCALR_CLI_SHA256_ARM64", + fetch_sha_from_sumsfile(sums, f"scalr-cli_{version}_linux_arm64.zip"), + ) + + +def refresh_aws_ssm_plugin_shas(version: str) -> None: + base = f"https://s3.amazonaws.com/session-manager-downloads/plugin/{version}" + write_value( + "AWS_SSM_PLUGIN_SHA256_AMD64", + compute_sha256_url(f"{base}/ubuntu_64bit/session-manager-plugin.deb"), + ) + write_value( + "AWS_SSM_PLUGIN_SHA256_ARM64", + compute_sha256_url(f"{base}/ubuntu_arm64/session-manager-plugin.deb"), + ) + + +# --- main flow -------------------------------------------------------------- + + +def bump( + label: str, + key: str, + latest: str, + current: str, + changes: list[tuple[str, str, str]], + refresh_shas: Callable[[str], None] | None = None, + update_readme: Callable[[str], None] | None = None, + section: str = SECTION_DEFAULT, +) -> bool: + if latest and current != latest: + log.info(f"{label}: {current} -> {latest}") + write_value(key, latest, section) + if refresh_shas: + refresh_shas(latest) + if update_readme: + update_readme(latest) + changes.append((label, current, latest)) + return True + log.info(f"{label}: {current} (up to date)") + return False + + +def main() -> int: + if not VERSIONS_FILE.exists(): + log.error(f"{VERSIONS_FILE} not found (run from repo root)") + return 1 + + log.info("Fetching latest versions...") + vs = read_versions() + base = read_versions(SECTION_BASE) + changes: list[tuple[str, str, str]] = [] + + # DEBIAN_BASE_IMAGE is read-only here — the base image (e.g. debian:trixie-slim) + # is a deliberate human choice and must not be auto-bumped. We only refresh the + # digest pinning for whatever image is currently configured. + debian_image = base.get("DEBIAN_BASE_IMAGE", "") + if not debian_image: + log.error(f"DEBIAN_BASE_IMAGE missing from {VERSIONS_FILE} ({SECTION_BASE})") + return 1 + bump( + "debian_base_digest", + "DEBIAN_BASE_DIGEST", + get_debian_base_digest(debian_image), + base.get("DEBIAN_BASE_DIGEST", ""), + changes, + section=SECTION_BASE, + ) + bump( + "kubectl", + "KUBECTL_VERSION", + get_latest_kubectl(), + vs.get("KUBECTL_VERSION", ""), + changes, + refresh_kubectl_shas, + update_readme_kubectl, + ) + bump( + "gcloud", + "GCLOUD_VERSION", + get_latest_gcloud(), + vs.get("GCLOUD_VERSION", ""), + changes, + refresh_gcloud_shas, + update_readme_gcloud, + ) + bump( + "aws_cli", + "AWS_CLI_VERSION", + get_latest_aws_cli(), + vs.get("AWS_CLI_VERSION", ""), + changes, + refresh_aws_cli_shas, + update_readme_aws_cli, + ) + bump( + "azure_cli", + "AZURE_CLI_VERSION", + get_latest_azure_cli(), + vs.get("AZURE_CLI_VERSION", ""), + changes, + None, + update_readme_azure_cli, + ) + bump( + "scalr_cli", + "SCALR_CLI_VERSION", + get_latest_scalr_cli(), + vs.get("SCALR_CLI_VERSION", ""), + changes, + refresh_scalr_cli_shas, + update_readme_scalr_cli, + ) + + # Python has two coupled fields (version + release) and one SHA refresh. + cur_v = vs.get("PYTHON_VERSION", "") + cur_r = vs.get("PYTHON_RELEASE", "") + lat_v, lat_r = get_latest_python_info() + py_changed = False + if lat_v and cur_v != lat_v: + log.info(f"python: {cur_v} -> {lat_v}") + write_value("PYTHON_VERSION", lat_v) + update_readme_python(lat_v) + changes.append(("python", cur_v, lat_v)) + py_changed = True + else: + log.info(f"python: {cur_v} (up to date)") + if lat_r and cur_r != lat_r: + log.info(f"python_release: {cur_r} -> {lat_r}") + write_value("PYTHON_RELEASE", lat_r) + changes.append(("python_release", cur_r, lat_r)) + py_changed = True + else: + log.info(f"python_release: {cur_r} (up to date)") + if py_changed: + refresh_python_shas(lat_v, lat_r) + + # Python 3.9 variant — same upstream as 3.14, override block with plain PYTHON_* keys. + vs39 = read_versions(SECTION_PY39) + cur_v39 = vs39.get("PYTHON_VERSION", "") + cur_r39 = vs39.get("PYTHON_RELEASE", "") + lat_v39, lat_r39 = get_latest_python39_info() + if not (lat_v39 and lat_r39): + log.warning("python39: no 3.9 build found in recent python-build-standalone releases; skipping") + else: + py39_changed = False + if cur_v39 != lat_v39: + log.info(f"python39: {cur_v39} -> {lat_v39}") + write_value("PYTHON_VERSION", lat_v39, SECTION_PY39) + changes.append(("python39", cur_v39, lat_v39)) + py39_changed = True + else: + log.info(f"python39: {cur_v39} (up to date)") + if cur_r39 != lat_r39: + log.info(f"python39_release: {cur_r39} -> {lat_r39}") + write_value("PYTHON_RELEASE", lat_r39, SECTION_PY39) + changes.append(("python39_release", cur_r39, lat_r39)) + py39_changed = True + else: + log.info(f"python39_release: {cur_r39} (up to date)") + if py39_changed: + refresh_python_shas(lat_v39, lat_r39, SECTION_PY39) + + # AWS SSM Plugin: version is manually pinned (no upstream version index), + # but its SHAs are refreshed in case the pinned version was hand-edited. + cur_ssm = vs.get("AWS_SSM_PLUGIN_VERSION", "") + if cur_ssm: + log.info(f"aws_ssm_plugin: {cur_ssm} (manually pinned; refreshing SHAs)") + refresh_aws_ssm_plugin_shas(cur_ssm) + else: + log.warning(f"aws_ssm_plugin: no version pinned in {VERSIONS_FILE}") + + if changes: + log.info("Summary of changes:") + for label, old, new in changes: + log.info(f" - {label}: {old} -> {new}") + else: + log.info("All versions are up to date.") + + log.info("Done!") + + if changes: + print() + for label, old, new in changes: + print(f"- {label}: {old} -> {new}") + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except urllib.error.URLError as e: + log.error(f"network error: {e}") + sys.exit(1) + except KeyboardInterrupt: + sys.exit(130) diff --git a/bump-versions.sh b/bump-versions.sh deleted file mode 100755 index 71018ae..0000000 --- a/bump-versions.sh +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env bash -# -# Script to bump all versions in ./versions file from their upstream sources. -# -set -euo pipefail - -VERSIONS_FILE="./versions" -README_FILE="./README.md" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } - -# Check required tools -for cmd in curl jq; do - if ! command -v "$cmd" &> /dev/null; then - log_error "$cmd is required but not installed." - exit 1 - fi -done - -# Function to get current version from versions file -get_current_version() { - local key="$1" - grep "^${key}=" "$VERSIONS_FILE" | cut -d'=' -f2 -} - -# Function to update version in versions file -update_version() { - local key="$1" - local new_version="$2" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/^${key}=.*/${key}=${new_version}/" "$VERSIONS_FILE" - else - sed -i "s/^${key}=.*/${key}=${new_version}/" "$VERSIONS_FILE" - fi -} - -# Function to update version in README.md (docker build args) -update_readme_version() { - local build_arg="$1" - local new_version="$2" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/--build-arg ${build_arg}=[^ ]*/--build-arg ${build_arg}=${new_version}/" "$README_FILE" - else - sed -i "s/--build-arg ${build_arg}=[^ ]*/--build-arg ${build_arg}=${new_version}/" "$README_FILE" - fi -} - -# Function to update Python version in Included Tools section -# Format: Python ([v3.13.3](https://www.python.org/downloads/release/python-3133/)) -update_readme_python() { - local new_version="$1" - local version_no_dots="${new_version//./}" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' -E "s|Python \(\[v[0-9.]+\]\(https://www\.python\.org/downloads/release/python-[0-9]+/\)\)|Python ([v${new_version}](https://www.python.org/downloads/release/python-${version_no_dots}/))|" "$README_FILE" - else - sed -i -E "s|Python \(\[v[0-9.]+\]\(https://www\.python\.org/downloads/release/python-[0-9]+/\)\)|Python ([v${new_version}](https://www.python.org/downloads/release/python-${version_no_dots}/))|" "$README_FILE" - fi -} - -# Function to update AWS CLI version in Included Tools section -# Format: AWS CLI ([2.27.1](https://github.com/aws/aws-cli/releases/tag/2.27.32)) -update_readme_aws() { - local new_version="$1" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' -E "s|AWS CLI \(\[[0-9.]+\]\(https://github\.com/aws/aws-cli/releases/tag/[0-9.]+\)\)|AWS CLI ([${new_version}](https://github.com/aws/aws-cli/releases/tag/${new_version}))|" "$README_FILE" - else - sed -i -E "s|AWS CLI \(\[[0-9.]+\]\(https://github\.com/aws/aws-cli/releases/tag/[0-9.]+\)\)|AWS CLI ([${new_version}](https://github.com/aws/aws-cli/releases/tag/${new_version}))|" "$README_FILE" - fi -} - -# Function to update Azure CLI version in Included Tools section -# Format: Azure CLI ([2.71.0](https://github.com/Azure/azure-cli/releases/tag/azure-cli-2.74.0)) -update_readme_azure() { - local new_version="$1" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' -E "s|Azure CLI \(\[[0-9.]+\]\(https://github\.com/Azure/azure-cli/releases/tag/azure-cli-[0-9.]+\)\)|Azure CLI ([${new_version}](https://github.com/Azure/azure-cli/releases/tag/azure-cli-${new_version}))|" "$README_FILE" - else - sed -i -E "s|Azure CLI \(\[[0-9.]+\]\(https://github\.com/Azure/azure-cli/releases/tag/azure-cli-[0-9.]+\)\)|Azure CLI ([${new_version}](https://github.com/Azure/azure-cli/releases/tag/azure-cli-${new_version}))|" "$README_FILE" - fi -} - -# Function to update Google Cloud SDK version in Included Tools section -# Format: Google Cloud SDK ([525.0.0](https://cloud.google.com/sdk/docs/release-notes#52500_2025-06-03)) -update_readme_gcloud() { - local new_version="$1" - local version_no_dots="${new_version//./}" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' -E "s|Google Cloud SDK \(\[[0-9.]+\]\(https://cloud\.google\.com/sdk/docs/release-notes#[^)]+\)\)|Google Cloud SDK ([${new_version}](https://cloud.google.com/sdk/docs/release-notes#${version_no_dots}))|" "$README_FILE" - else - sed -i -E "s|Google Cloud SDK \(\[[0-9.]+\]\(https://cloud\.google\.com/sdk/docs/release-notes#[^)]+\)\)|Google Cloud SDK ([${new_version}](https://cloud.google.com/sdk/docs/release-notes#${version_no_dots}))|" "$README_FILE" - fi -} - -# Function to update Kubectl version in Included Tools section -# Format: Kubectl ([0.33.1](https://github.com/kubernetes/kubectl/releases/tag/v0.33.1)) -# Note: kubectl repo uses v0.x.y versioning matching k8s minor version -update_readme_kubectl() { - local new_version="$1" # e.g., v1.35.0 - # Convert v1.35.0 to 0.35.0 for kubectl repo versioning - local kubectl_version="${new_version//v1./0.}" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' -E "s|Kubectl \(\[[0-9.]+\]\(https://github\.com/kubernetes/kubectl/releases/tag/v[0-9.]+\)\)|Kubectl ([${kubectl_version}](https://github.com/kubernetes/kubectl/releases/tag/v${kubectl_version}))|" "$README_FILE" - else - sed -i -E "s|Kubectl \(\[[0-9.]+\]\(https://github\.com/kubernetes/kubectl/releases/tag/v[0-9.]+\)\)|Kubectl ([${kubectl_version}](https://github.com/kubernetes/kubectl/releases/tag/v${kubectl_version}))|" "$README_FILE" - fi -} - -# Function to update Scalr CLI version in Included Tools section -# Format: Scalr CLI ([0.17.1](https://github.com/Scalr/scalr-cli/releases/tag/v0.17.1)) -update_readme_scalr() { - local new_version="$1" - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' -E "s|Scalr CLI \(\[[0-9.]+\]\(https://github\.com/Scalr/scalr-cli/releases/tag/v[0-9.]+\)\)|Scalr CLI ([${new_version}](https://github.com/Scalr/scalr-cli/releases/tag/v${new_version}))|" "$README_FILE" - else - sed -i -E "s|Scalr CLI \(\[[0-9.]+\]\(https://github\.com/Scalr/scalr-cli/releases/tag/v[0-9.]+\)\)|Scalr CLI ([${new_version}](https://github.com/Scalr/scalr-cli/releases/tag/v${new_version}))|" "$README_FILE" - fi -} - -# Fetch latest kubectl version -get_latest_kubectl() { - curl -sL https://dl.k8s.io/release/stable.txt -} - -# Fetch latest gcloud version -get_latest_gcloud() { - # Get latest version from Google Cloud SDK release notes page - curl -sL "https://cloud.google.com/sdk/docs/release-notes" 2>/dev/null | \ - grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "" -} - -# Fetch latest AWS CLI v2 version -get_latest_aws_cli() { - curl -sL "https://api.github.com/repos/aws/aws-cli/tags" | \ - jq -r '.[].name | select(startswith("2."))' | head -1 -} - -# Fetch latest Azure CLI version -get_latest_azure_cli() { - curl -sL "https://api.github.com/repos/Azure/azure-cli/releases/latest" | \ - jq -r '.tag_name' | sed 's/^azure-cli-//' -} - -# Fetch latest Scalr CLI version -get_latest_scalr_cli() { - curl -sL "https://api.github.com/repos/Scalr/scalr-cli/releases/latest" | \ - jq -r '.tag_name' | sed 's/^v//' -} - -# Fetch latest Python version and release from python-build-standalone -# Returns: "version release" (e.g., "3.13.11 20260114") -get_latest_python_info() { - local release_info - release_info=$(curl -sL "https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest") - local version release - version=$(echo "$release_info" | jq -r '.assets[].name' | grep -oE 'cpython-3\.14\.[0-9]+' | sed 's/cpython-//' | head -1) - release=$(echo "$release_info" | jq -r '.tag_name') - echo "$version $release" -} - -# Track changes (compatible with bash 3.x) -CHANGES="" -CHANGES_COUNT=0 - -add_change() { - local key="$1" - local old_ver="$2" - local new_ver="$3" - if [[ -n "$CHANGES" ]]; then - CHANGES="${CHANGES}|${key}:${old_ver}:${new_ver}" - else - CHANGES="${key}:${old_ver}:${new_ver}" - fi - CHANGES_COUNT=$((CHANGES_COUNT + 1)) -} - -log_info "Fetching latest versions..." - -# Kubectl -CURRENT_KUBECTL=$(get_current_version "kubectl") -LATEST_KUBECTL=$(get_latest_kubectl) -if [[ -n "$LATEST_KUBECTL" && "$CURRENT_KUBECTL" != "$LATEST_KUBECTL" ]]; then - log_info "kubectl: $CURRENT_KUBECTL -> $LATEST_KUBECTL" - update_version "kubectl" "$LATEST_KUBECTL" - update_readme_version "KUBECTL_VERSION" "$LATEST_KUBECTL" - update_readme_kubectl "$LATEST_KUBECTL" - add_change "kubectl" "$CURRENT_KUBECTL" "$LATEST_KUBECTL" -else - log_info "kubectl: $CURRENT_KUBECTL (up to date)" -fi - -# GCloud -CURRENT_GCLOUD=$(get_current_version "gcloud") -LATEST_GCLOUD=$(get_latest_gcloud) -if [[ -n "$LATEST_GCLOUD" && "$CURRENT_GCLOUD" != "$LATEST_GCLOUD" ]]; then - log_info "gcloud: $CURRENT_GCLOUD -> $LATEST_GCLOUD" - update_version "gcloud" "$LATEST_GCLOUD" - update_readme_version "GCLOUD_VERSION" "$LATEST_GCLOUD" - update_readme_gcloud "$LATEST_GCLOUD" - add_change "gcloud" "$CURRENT_GCLOUD" "$LATEST_GCLOUD" -else - log_info "gcloud: $CURRENT_GCLOUD (up to date)" -fi - -# AWS CLI -CURRENT_AWS=$(get_current_version "aws_cli") -LATEST_AWS=$(get_latest_aws_cli) -if [[ -n "$LATEST_AWS" && "$CURRENT_AWS" != "$LATEST_AWS" ]]; then - log_info "aws_cli: $CURRENT_AWS -> $LATEST_AWS" - update_version "aws_cli" "$LATEST_AWS" - update_readme_version "AWS_CLI_VERSION" "$LATEST_AWS" - update_readme_aws "$LATEST_AWS" - add_change "aws_cli" "$CURRENT_AWS" "$LATEST_AWS" -else - log_info "aws_cli: $CURRENT_AWS (up to date)" -fi - -# Azure CLI -CURRENT_AZURE=$(get_current_version "azure_cli") -LATEST_AZURE=$(get_latest_azure_cli) -if [[ -n "$LATEST_AZURE" && "$CURRENT_AZURE" != "$LATEST_AZURE" ]]; then - log_info "azure_cli: $CURRENT_AZURE -> $LATEST_AZURE" - update_version "azure_cli" "$LATEST_AZURE" - update_readme_version "AZURE_CLI_VERSION" "$LATEST_AZURE" - update_readme_azure "$LATEST_AZURE" - add_change "azure_cli" "$CURRENT_AZURE" "$LATEST_AZURE" -else - log_info "azure_cli: $CURRENT_AZURE (up to date)" -fi - -# Scalr CLI -CURRENT_SCALR=$(get_current_version "scalr_cli") -LATEST_SCALR=$(get_latest_scalr_cli) -if [[ -n "$LATEST_SCALR" && "$CURRENT_SCALR" != "$LATEST_SCALR" ]]; then - log_info "scalr_cli: $CURRENT_SCALR -> $LATEST_SCALR" - update_version "scalr_cli" "$LATEST_SCALR" - update_readme_version "SCALR_CLI_VERSION" "$LATEST_SCALR" - update_readme_scalr "$LATEST_SCALR" - add_change "scalr_cli" "$CURRENT_SCALR" "$LATEST_SCALR" -else - log_info "scalr_cli: $CURRENT_SCALR (up to date)" -fi - -# Python (version and release) -CURRENT_PYTHON=$(get_current_version "python") -CURRENT_PYTHON_RELEASE=$(get_current_version "python_release") -read -r LATEST_PYTHON LATEST_PYTHON_RELEASE <<< "$(get_latest_python_info)" -if [[ -n "$LATEST_PYTHON" && "$CURRENT_PYTHON" != "$LATEST_PYTHON" ]]; then - log_info "python: $CURRENT_PYTHON -> $LATEST_PYTHON" - update_version "python" "$LATEST_PYTHON" - update_readme_version "PYTHON_VERSION" "$LATEST_PYTHON" - update_readme_python "$LATEST_PYTHON" - add_change "python" "$CURRENT_PYTHON" "$LATEST_PYTHON" -else - log_info "python: $CURRENT_PYTHON (up to date)" -fi -if [[ -n "$LATEST_PYTHON_RELEASE" && "$CURRENT_PYTHON_RELEASE" != "$LATEST_PYTHON_RELEASE" ]]; then - log_info "python_release: $CURRENT_PYTHON_RELEASE -> $LATEST_PYTHON_RELEASE" - update_version "python_release" "$LATEST_PYTHON_RELEASE" - update_readme_version "PYTHON_RELEASE" "$LATEST_PYTHON_RELEASE" - add_change "python_release" "$CURRENT_PYTHON_RELEASE" "$LATEST_PYTHON_RELEASE" -else - log_info "python_release: $CURRENT_PYTHON_RELEASE (up to date)" -fi - -# Print summary -if [[ $CHANGES_COUNT -gt 0 ]]; then - echo "" - log_info "Summary of changes:" - IFS='|' read -ra CHANGE_ARRAY <<< "$CHANGES" - for change in "${CHANGE_ARRAY[@]}"; do - IFS=':' read -ra PARTS <<< "$change" - echo " - ${PARTS[0]}: ${PARTS[1]} -> ${PARTS[2]}" - done -else - log_info "All versions are up to date." -fi - -echo "" -log_info "Done!" diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..ebb57f1 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,60 @@ +# Docker Buildx Bake file for the Scalr runner image. +# +# Build all variants: +# docker buildx bake -f docker-bake.hcl -f versions.json +# +# Build one variant: +# docker buildx bake -f docker-bake.hcl -f versions.json full +# docker buildx bake -f docker-bake.hcl -f versions.json python39 +# docker buildx bake -f docker-bake.hcl -f versions.json slim +# +# Override the version tag (defaults to "dev" for local builds): +# VERSION=3.0.0 docker buildx bake -f docker-bake.hcl -f versions.json full + +variable "VERSION" { + default = "dev" +} + +# Versions and SHA256 checksums for tools installed inside the image. +# Populated from versions.json (a native bake variable file) and maintained +# by ./bump-versions.py. +# versions_base — base layer (Debian base digest); used by every target +# versions_full — extra tools for the full image (kubectl, cloud CLIs, Python 3.14, …) +# versions_python39 — overrides merged on top for the -python39 image +variable "versions_base" { + default = {} +} +variable "versions_full" { + default = {} +} +variable "versions_python39" { + default = {} +} + +group "default" { + targets = ["full", "python39", "slim"] +} + +target "full" { + target = "full" + platforms = ["linux/amd64", "linux/arm64"] + args = merge(versions_base, versions_full) + tags = ["scalr/runner:${VERSION}"] + cache-from = ["type=registry,ref=scalr/runner:buildcache"] +} + +target "python39" { + target = "full" + platforms = ["linux/amd64", "linux/arm64"] + args = merge(versions_base, versions_full, versions_python39) + tags = ["scalr/runner:${VERSION}-python39"] + cache-from = ["type=registry,ref=scalr/runner:buildcache-python39"] +} + +target "slim" { + target = "slim" + platforms = ["linux/amd64", "linux/arm64"] + args = versions_base + tags = ["scalr/runner:${VERSION}-slim"] + cache-from = ["type=registry,ref=scalr/runner:buildcache-slim"] +} diff --git a/versions b/versions deleted file mode 100644 index 8c9bce9..0000000 --- a/versions +++ /dev/null @@ -1,7 +0,0 @@ -kubectl=v1.35.3 -gcloud=564.0.0 -aws_cli=2.34.29 -azure_cli=2.85.0 -scalr_cli=0.17.8 -python=3.14.4 -python_release=20260408 diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..2278c10 --- /dev/null +++ b/versions.json @@ -0,0 +1,45 @@ +{ + "variable": { + "versions_base": { + "default": { + "DEBIAN_BASE_IMAGE": "debian:trixie-slim", + "DEBIAN_BASE_DIGEST": "sha256:b6e2a152f22a40ff69d92cb397223c906017e1391a73c952b588e51af8883bf8" + } + }, + "versions_full": { + "default": { + "KUBECTL_VERSION": "v1.36.1", + "KUBECTL_SHA256_AMD64": "629d3f410e09bf49b64ae7079f7f0bda1191efed311f7d37fdbab0ad5b0ec2b7", + "KUBECTL_SHA256_ARM64": "59f7ee8e477fae658447607dc3c8790ac17a1b016c01c622c12070e969e2d4e7", + "GCLOUD_VERSION": "569.0.0", + "GCLOUD_SHA256_AMD64": "d242be22b34ef69b17925cc57022dae8ba8c02393cf7f5954b8fd105201b17e5", + "GCLOUD_SHA256_ARM64": "e2ec1a30d714162e7020ad462cba344bd90e24004fe2b53637a57d7ac7715bec", + "AWS_CLI_VERSION": "2.34.53", + "AWS_CLI_SHA256_AMD64": "b06c9865123cf1bc6f8761a0bf3161630e6f55d84363f9fda89535e2cdbe654b", + "AWS_CLI_SHA256_ARM64": "1883e4dddee2ecfab39df0ff0f13b32d50715f7ad9f02a322ecd73ee439cef8f", + "AZURE_CLI_VERSION": "2.86.0", + "SCALR_CLI_VERSION": "0.18.0", + "SCALR_CLI_SHA256_AMD64": "5321bcebd48d30f7c3a936a5c5f10d9d3cfb77ef2974575d4df8e7ee9f890589", + "SCALR_CLI_SHA256_ARM64": "8dcada09b5bf10001f4e83190fc9c2a66df692beb07a74b4afe503d2fa75629e", + "PYTHON_VERSION": "3.14.5", + "PYTHON_RELEASE": "20260510", + "PYTHON_SHA256_AMD64": "daa4e5322b2b971011a9455ba813ecdb927ed6832fe87bd3d09162b3dc6fef2c", + "PYTHON_SHA256_ARM64": "c70c9c3f35c3fac4e211608409a15dc0c5522e3956e82793f724a6eee64e0ff5", + "AWS_SSM_PLUGIN_VERSION": "1.2.814.0", + "AWS_SSM_PLUGIN_SHA256_AMD64": "67ddd12b963c28e44126f9732075caae333cc5f55e902623bd6a107ead2bcc56", + "AWS_SSM_PLUGIN_SHA256_ARM64": "dd714895b7e9b9945139301311778c175e556f75fda25250d36cff19c5eb0cdf" + } + }, + "versions_python39": { + "default": { + "PYTHON_VERSION": "3.9.25", + "PYTHON_RELEASE": "20251031", + "PYTHON_SHA256_AMD64": "5afeb1b01609195371a11b2fe6a3e73a315ff6baffd5d3c4ffa2ac3c6245acf5", + "PYTHON_SHA256_ARM64": "1ff8edb17b2448883c56ae1046a9c55db604ed52800febe7cd0cdebb7d39fceb", + "GCLOUD_VERSION": "564.0.0", + "GCLOUD_SHA256_AMD64": "e6e85df426861590178505dde16e1c7c06c806d24a6e2f62b2bdc39f677371c7", + "GCLOUD_SHA256_ARM64": "92f7581e434ae44f89d379936e6e7a20f0c9a78439b5834f9b21bde8b3ed8ec6" + } + } + } +}