├── .bazelignore ├── .bazelrc ├── .bazelversion ├── .cloudbuild ├── cloudbuild.yaml └── release.sh ├── .github ├── ISSUE_TEMPLATE │ ├── actionable-debian-cve.md │ ├── actionable-non-debian-cve.md │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── buildifier.yaml │ ├── ci.bazelrc │ ├── ci.yaml │ ├── examples.yaml │ ├── image-check.yaml │ ├── scorecards-analysis.yml │ ├── update-deb-package-snapshots.yml │ ├── update-node-archives.yml │ └── update-temurin-packages.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .travis.yml ├── BUILD ├── CONTRIBUTING.md ├── LICENSE ├── MODULE.bazel ├── MODULE.bazel.lock ├── README.md ├── RELEASES.md ├── SECURITY.md ├── SUPPORT_POLICY.md ├── WORKSPACE ├── base ├── BUILD ├── README.md ├── base.bzl ├── distro.bzl ├── nsswitch.tar ├── test.sh └── testdata │ ├── base.yaml │ ├── certs.yaml │ ├── check_certs.go │ ├── debian12.yaml │ ├── debug.yaml │ └── static.yaml ├── cc ├── BUILD └── README.md ├── checksums.bzl ├── common ├── BUILD.bazel └── variables.bzl ├── cosign.pub ├── examples ├── BUILD ├── cc │ ├── BUILD │ ├── Dockerfile │ ├── hello.c │ ├── hello_cc.cc │ └── testdata │ │ ├── hello_cc_debian12.yaml │ │ └── hello_debian12.yaml ├── go │ ├── BUILD │ ├── Dockerfile │ ├── go.mod │ ├── main.go │ └── main_test.go ├── java │ ├── BUILD │ ├── Dockerfile │ ├── HelloJava.java │ └── testdata │ │ ├── hello_nonroot_debian12.yaml │ │ └── hello_root_debian12.yaml ├── nodejs │ ├── BUILD │ ├── Dockerfile │ ├── hello.js │ ├── hello_http.js │ ├── node-express │ │ ├── Dockerfile │ │ ├── hello_express.js │ │ └── package.json │ ├── package-lock.json │ ├── package.json │ └── testdata │ │ └── hello.yaml ├── nonroot │ ├── BUILD │ └── testdata │ │ ├── user.go │ │ └── user.yaml ├── python3-requirements │ ├── Dockerfile │ ├── README.md │ ├── psutil_example.py │ └── requirements.txt ├── python3 │ ├── BUILD │ ├── Dockerfile │ └── hello.py ├── rust │ ├── .dockerignore │ ├── BUILD │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Dockerfile │ └── src │ │ └── main.rs └── test.sh ├── experimental ├── BUILD └── busybox │ ├── BUILD │ └── commands.bzl ├── java ├── BUILD ├── README.md ├── control ├── jre_ver.bzl └── testdata │ ├── CheckCerts.java │ ├── CheckEncoding.java │ ├── CheckLibharfbuzz.java │ ├── java17_debian12.yaml │ ├── java17_debug_debian12.yaml │ ├── java21_debian12.yaml │ ├── java21_debug_debian12.yaml │ ├── java_base.yaml │ ├── java_base_debug.yaml │ ├── java_certs.yaml │ ├── java_encoding.yaml │ └── java_libharfbuzz.yaml ├── knife ├── nodejs ├── BUILD ├── README.md ├── control ├── node_arch.bzl └── testdata │ ├── check_certificate.js │ ├── check_certificate.yaml │ ├── check_headers.yaml │ ├── check_npm.yaml │ ├── nodejs20.yaml │ ├── nodejs22.yaml │ └── nodejs24.yaml ├── private ├── extensions │ ├── BUILD.bazel │ ├── busybox.bzl │ ├── java.bzl │ ├── node.bzl │ └── version.bzl ├── oci │ ├── BUILD.bazel │ ├── cc_image.bzl │ ├── defs.bzl │ ├── digest.bzl │ ├── go_image.bzl │ ├── java_image.bzl │ ├── rust_image.bzl │ ├── sign_and_push.bzl │ └── sign_and_push.sh.tpl ├── pkg │ ├── BUILD.bazel │ ├── debian_spdx.bzl │ ├── debian_spdx.go │ ├── oci_image_spdx.bzl │ ├── oci_image_spdx.go │ └── test │ │ └── oci_image │ │ ├── BUILD.bazel │ │ ├── fat_image_sbom.spdx.json │ │ ├── image_amd64.spdx.json │ │ └── image_arm64.spdx.json ├── repos │ ├── BUILD.bazel │ ├── deb │ │ ├── BUILD.bazel │ │ ├── bookworm.lock.json │ │ ├── bookworm.yaml │ │ ├── bookworm_java.lock.json │ │ ├── bookworm_java.yaml │ │ ├── bookworm_python.lock.json │ │ ├── bookworm_python.yaml │ │ ├── deb.MODULE.bazel │ │ └── package.BUILD.tmpl │ └── java.MODULE.bazel ├── stamp.bash ├── tools │ ├── BUILD.bazel │ └── diff.bash └── util │ ├── BUILD │ ├── deb.bzl │ ├── extract.bzl │ ├── java_cacerts.bzl │ └── merge_providers.bzl ├── python3 ├── BUILD ├── README.md └── testdata │ ├── debian12.yaml │ └── python3.yaml └── scripts ├── update_java_archives.sh └── update_node_archives.js /.bazelignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/distroless/4c40ba78166a7e559bcd41df2083d45bdb4212e7/.bazelignore -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | # Bazel settings that apply to this repository. 2 | # Take care to document any settings that you expect users to apply. 3 | # Settings that apply only to CI are in .github/workflows/ci.bazelrc 4 | 5 | # Allow DOCKER_HOST env to leak into test actions. 6 | test --test_env=DOCKER_HOST 7 | 8 | # Stamp 9 | build:release --workspace_status_command=./private/stamp.bash --stamp 10 | run:release --workspace_status_command=./private/stamp.bash --stamp 11 | test:release --workspace_status_command=./private/stamp.bash --stamp 12 | 13 | # Allow external dependencies to be retried. debian snapshot is unreliable and needs retries. 14 | common --experimental_repository_downloader_retries=20 15 | common --http_timeout_scaling=2.0 16 | 17 | # Enable platform specific options 18 | build --enable_platform_specific_config 19 | 20 | # Use a hermetic Java version 21 | build --java_runtime_version=remotejdk_11 22 | 23 | # Newer versions jdk creates collisions on /tmp 24 | # See: https://github.com/bazelbuild/bazel/issues/3236 25 | # https://github.com/GoogleContainerTools/rules_distroless/actions/runs/7118944984/job/19382981899?pr=9#step:8:51 26 | common:linux --sandbox_tmpfs_path=/tmp 27 | 28 | # Load any settings specific to the current user. 29 | # .bazelrc.user should appear in .gitignore so that settings are not shared with team members 30 | # This needs to be last statement in this 31 | # config, as the user configuration should be able to overwrite flags from this file. 32 | # See https://docs.bazel.build/versions/master/best-practices.html#bazelrc 33 | # (Note that we use .bazelrc.user so the file appears next to .bazelrc in directory listing, 34 | # rather than user.bazelrc as suggested in the Bazel docs) 35 | try-import %workspace%/.bazelrc.user 36 | -------------------------------------------------------------------------------- /.bazelversion: -------------------------------------------------------------------------------- 1 | 7.4.0 -------------------------------------------------------------------------------- /.cloudbuild/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | timeout: 10800s # 3 hours 2 | 3 | options: 4 | machineType: N1_HIGHCPU_8 5 | logging: CLOUD_LOGGING_ONLY 6 | 7 | artifacts: 8 | objects: 9 | location: 'gs://${_ARTIFACTS_GCS_}/logs' 10 | paths: ['.logs/*.log'] 11 | 12 | steps: 13 | - name: gcr.io/cloud-builders/bazel@sha256:e82204cf0715edb80e4957fa55f944b9dd06b0b1c4eb4b0ca0022de7839726f6 14 | env: 15 | - PROJECT_ID=${PROJECT_ID} 16 | - COMMIT_SHA=${COMMIT_SHA} 17 | - REGISTRY=gcr.io 18 | - REMOTE_CACHE_GCS=${_REMOTE_CACHE_GCS_} 19 | - KEYLESS=keyless@${PROJECT_ID}.iam.gserviceaccount.com 20 | entrypoint: bash 21 | args: [".cloudbuild/release.sh"] 22 | -------------------------------------------------------------------------------- /.cloudbuild/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit -o xtrace -o pipefail 3 | 4 | # setup remote cache 5 | curl -fsSL https://github.com/buchgr/bazel-remote/releases/download/v2.4.0/bazel-remote-2.4.0-linux-x86_64 -o bazel-remote 6 | echo '717a44dd526c574b0a0edda1159f5795cc1b2257db1d519280a3d7a9c5addde5 bazel-remote' | sha256sum --check 7 | chmod +x bazel-remote 8 | mkdir .logs 9 | ./bazel-remote --max_size 8 --dir ~/.cache/bazel-remote --experimental_remote_asset_api --grpc_address 0.0.0.0:4700 --gcs_proxy.bucket $REMOTE_CACHE_GCS --gcs_proxy.use_default_credentials > .logs/bazel-remote.log 2>&1 & 10 | 11 | # install bazelisk (TODO: there's probably a better way to do this) 12 | curl -fsSL https://github.com/bazelbuild/bazelisk/releases/download/v1.21.0/bazelisk-linux-amd64 -o bazelisk 13 | echo '655a5c675dacf3b7ef4970688b6a54598aa30cbaa0b9e717cd1412c1ef9ec5a7 bazelisk' | sha256sum --check 14 | chmod a+x bazelisk 15 | 16 | # setup remote caching and remote asset API. 17 | echo "common --remote_cache=grpc://0.0.0.0:4700" >> ~/.bazelrc 18 | echo "common --experimental_remote_downloader=grpc://0.0.0.0:4700" >> ~/.bazelrc 19 | echo "common --google_default_credentials" >> ~/.bazelrc 20 | echo "common --announce_rc" >> ~/.bazelrc 21 | 22 | for i in $(seq 5); do 23 | ./bazelisk cquery 'kind(merge_providers, deps(kind(oci_image, ...)))' --output=label --config=release && break || sleep 20; 24 | done 25 | ./bazelisk run :sign_and_push --config=release -- --keyless $KEYLESS 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/actionable-debian-cve.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Actionable debian CVE 3 | about: A CVE from a debian package where a fix is available 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I have read the [SECURITY.md](https://github.com/GoogleContainerTools/distroless/blob/main/SECURITY.md) 11 | - [ ] I understand that this repo tracks debian package releases and cannot fix debian CVEs on its own 12 | - [ ] this CVE shows a fix is available in the appropriate debian version (bookworm) and channel (main, security) *and* it has been more than 48 hours. 13 | 14 | Please describe the image you encountered this with and a link to the debian security tracker 15 | https://security-tracker.debian.org/tracker/CVE-XXXX-YYYYY 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/actionable-non-debian-cve.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Actionable non debian CVE 3 | about: A CVE is an package imported into distroless that is not from debian 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **CVE disclosure** 11 | A link to a public CVE disclosure 12 | 13 | **Name of image** 14 | The distroless image you are using 15 | 16 | **Link to updated package** 17 | A link to an available update for this package 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: There is an issue with how distroless is built 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Console Output** 20 | If applicable, add information from your container run 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "debian-package-manager/" 9 | schedule: 10 | interval: "daily" 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.github/workflows/buildifier.yaml: -------------------------------------------------------------------------------- 1 | name: Buildifier 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | autoformat: 12 | name: Auto-format and Check 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: "1.21" 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v4 23 | 24 | - name: Install Dependencies 25 | run: | 26 | go install github.com/bazelbuild/buildtools/buildifier@3.2.0 27 | 28 | - name: Run buildifier 29 | shell: bash 30 | run: | 31 | buildifier -mode=fix $(find . -name 'BUILD*' -o -name 'WORKSPACE*' -o -name '*.bzl' -type f) 32 | 33 | - name: Verify buildifier 34 | shell: bash 35 | run: | 36 | # From: https://backreference.org/2009/12/23/how-to-match-newlines-in-sed/ 37 | # This is to leverage this workaround: 38 | # https://github.com/actions/toolkit/issues/193#issuecomment-605394935 39 | function urlencode() { 40 | sed ':begin;$!N;s/\n/%0A/;tbegin' 41 | } 42 | if [[ $(git diff-index --name-only HEAD --) ]]; then 43 | for x in $(git diff-index --name-only HEAD --); do 44 | echo "::error file=$x::Please run buildifier.%0A$(git diff $x | urlencode)" 45 | done 46 | echo "${{ github.repository }} is out of style. Please run buildifier." 47 | exit 1 48 | fi 49 | echo "${{ github.repository }} is formatted correctly." 50 | -------------------------------------------------------------------------------- /.github/workflows/ci.bazelrc: -------------------------------------------------------------------------------- 1 | build --announce_rc 2 | build --repository_cache=~/.cache/bazel-repo 3 | # intentionally disable build cache, it gets too big 4 | # build --disk_cache=~/.cache/bazel 5 | 6 | common --curses=no 7 | test --test_output=errors 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: # Allow manual runs. 5 | pull_request: 6 | branches: ["main"] 7 | push: 8 | branches: ["main"] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | lockfile: 15 | name: Check lockfile up to date 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Check lockfile 20 | run: bazel mod deps --lockfile_mode=error 21 | 22 | ci-build-test: 23 | name: CI build and unit test 24 | runs-on: distroless-ci-large-ubuntu-22.04 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Mount bazel caches 28 | uses: actions/cache@v4 29 | with: 30 | path: | 31 | ~/.cache/bazel-repo 32 | key: bazel-cache-deps-ci1-${{ github.sha }} 33 | restore-keys: | 34 | bazel-cache-deps-ci1-${{ github.sha }} 35 | bazel-cache-deps-ci1- 36 | - name: Free space 37 | run: | 38 | sudo apt-get remove -y '^dotnet-.*' 39 | sudo apt-get remove -y '^llvm-.*' 40 | sudo apt-get remove -y 'php.*' 41 | sudo apt-get remove -y azure-cli google-cloud-cli google-chrome-stable firefox powershell mono-devel 42 | sudo apt-get autoremove -y 43 | sudo apt-get clean 44 | rm -rf /usr/share/dotnet/ 45 | - name: Fetch # this can take a long time if there are a lot of errors 46 | run: | 47 | for i in $(seq 10); do 48 | bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc fetch //... && break || sleep 180; 49 | done 50 | - name: Build All Images 51 | run: | 52 | set -e 53 | targets=$(bazel query 'kind(oci_image, deps(:sign_and_push))') 54 | bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc build $targets 55 | - name: Unit Tests 56 | run: bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc test //... --build_tests_only 57 | - name: Build Examples 58 | run: bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc build //examples/... 59 | 60 | ci-images: 61 | name: CI image tests 62 | runs-on: distroless-ci-large-ubuntu-22.04 63 | steps: 64 | - uses: actions/checkout@v4 65 | - name: Mount bazel caches 66 | uses: actions/cache@v4 67 | with: 68 | path: | 69 | ~/.cache/bazel-repo 70 | key: bazel-cache-deps-ci2-${{ github.sha }} 71 | restore-keys: | 72 | bazel-cache-deps-ci2-${{ github.sha }} 73 | bazel-cache-deps-ci2- 74 | - name: Free space 75 | run: | 76 | sudo apt-get remove -y '^dotnet-.*' 77 | sudo apt-get remove -y '^llvm-.*' 78 | sudo apt-get remove -y 'php.*' 79 | sudo apt-get remove -y azure-cli google-cloud-cli google-chrome-stable firefox powershell mono-devel 80 | sudo apt-get autoremove -y 81 | sudo apt-get clean 82 | rm -rf /usr/share/dotnet/ 83 | 84 | - name: Fetch 85 | run: | 86 | for i in $(seq 20); do 87 | bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc cquery 'attr(tags, "amd64", ...)' && break 88 | sleep 10; 89 | done 90 | 91 | - name: Image Tests 92 | run: bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc test $(bazel query 'attr(tags, "amd64", ...)') 93 | -------------------------------------------------------------------------------- /.github/workflows/examples.yaml: -------------------------------------------------------------------------------- 1 | name: Examples 2 | 3 | on: 4 | pull_request: 5 | branches: [ 'main' ] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | examples: 12 | name: Build and run examples 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: ./examples/test.sh 18 | -------------------------------------------------------------------------------- /.github/workflows/image-check.yaml: -------------------------------------------------------------------------------- 1 | name: Image Check 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: ["main"] 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | pull-requests: write 14 | 15 | jobs: 16 | diff: 17 | runs-on: distroless-ci-large-ubuntu-22.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: "1.21" 24 | - uses: actions/cache@v4 25 | with: 26 | path: | 27 | ~/.cache/bazel-repo 28 | key: bazel-cache-deps-ci2-${{ github.sha }} 29 | restore-keys: | 30 | bazel-cache-deps-ci2-${{ github.sha }} 31 | bazel-cache-deps-ci2- 32 | 33 | - name: Fetch 34 | run: | 35 | for i in $(seq 10); do 36 | bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc cquery 'attr(tags, "amd64", ...)' && break 37 | sleep 10; 38 | done 39 | 40 | - name: Build all images 41 | run: bazel build //:sign_and_push 42 | 43 | - name: Install Deps 44 | run: | 45 | go install github.com/google/go-containerregistry/cmd/crane@main 46 | go install github.com/reproducible-containers/diffoci/cmd/diffoci@master 47 | go install filippo.io/mkcert@master 48 | sudo curl -fsSL "https://github.com/project-zot/zot/releases/download/v2.0.2-rc2/zot-linux-amd64-minimal" > /usr/local/bin/zot 49 | sudo chmod +x /usr/local/bin/zot 50 | 51 | - name: Diff All Images 52 | id: diff 53 | run: | 54 | ./private/tools/diff.bash \ 55 | --query-bazel --registry-spawn-https \ 56 | --head-ref ${{ github.head_ref }} \ 57 | --base-ref ${{ github.event.pull_request.base.ref }} \ 58 | --set-github-output-on-diff \ 59 | --skip-image-index \ 60 | --jobs $(($(nproc --all) * 2)) \ 61 | --logs ./verbose.log \ 62 | --report ./report.log 63 | 64 | - uses: actions/upload-artifact@v4 65 | id: report 66 | with: 67 | name: "Report" 68 | path: | 69 | ./verbose.log 70 | ./report.log 71 | 72 | - uses: peter-evans/find-comment@v3 73 | id: fc 74 | if: ${{ !github.event.pull_request.head.repo.fork }} 75 | with: 76 | issue-number: ${{ github.event.pull_request.number }} 77 | comment-author: "github-actions[bot]" 78 | body-includes: 🌳 🔄 Image Check 79 | 80 | - name: Report diff 81 | if: ${{ !github.event.pull_request.head.repo.fork && steps.diff.outputs.changed_targets }} 82 | uses: peter-evans/create-or-update-comment@v4 83 | with: 84 | comment-id: ${{ steps.fc.outputs.comment-id }} 85 | issue-number: ${{ github.event.pull_request.number }} 86 | body: | 87 | 🌳 🔄 Image Check 88 | 89 | This pull request has modified the following images: 90 | 91 | ```starlark 92 | ${{steps.diff.outputs.changed_targets}} 93 | ``` 94 | 95 | You can check the details in the report [here](${{steps.report.outputs.artifact-url}}) 96 | edit-mode: replace 97 | 98 | - name: Report no diff 99 | if: ${{ !github.event.pull_request.head.repo.fork && !steps.diff.outputs.changed_targets }} 100 | uses: peter-evans/create-or-update-comment@v4 101 | with: 102 | comment-id: ${{ steps.fc.outputs.comment-id }} 103 | issue-number: ${{ github.event.pull_request.number }} 104 | body: | 105 | 🌳 🔄 Image Check 106 | This pull request doesn't make any changes to the images. 👍 107 | You can check the details in the report [here](${{steps.report.outputs.artifact-url}}) 108 | edit-mode: replace 109 | -------------------------------------------------------------------------------- /.github/workflows/scorecards-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards supply-chain security 2 | on: 3 | # Only the default branch is supported. 4 | branch_protection_rule: 5 | schedule: 6 | - cron: '38 10 * * 1' 7 | push: 8 | branches: [ main ] 9 | 10 | # Declare default permissions as read only. 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: Scorecards analysis 16 | runs-on: ubuntu-latest 17 | permissions: 18 | # Needed to upload the results to code-scanning dashboard. 19 | security-events: write 20 | id-token: write 21 | 22 | steps: 23 | - name: "Checkout code" 24 | uses: actions/checkout@v4 25 | with: 26 | persist-credentials: false 27 | 28 | - name: "Run analysis" 29 | uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 30 | with: 31 | results_file: results.sarif 32 | results_format: sarif 33 | # Read-only PAT token. 34 | repo_token: ${{ secrets.SCORECARD_TOKEN }} 35 | # Publish the results to enable scorecard badges. For more details, see 36 | # https://github.com/ossf/scorecard-action#publishing-results. 37 | publish_results: true 38 | 39 | # Upload the results as artifacts (optional). 40 | - name: "Upload artifact" 41 | uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 42 | with: 43 | name: SARIF file 44 | path: results.sarif 45 | retention-days: 5 46 | 47 | # Upload the results to GitHub's code scanning dashboard. 48 | - name: "Upload to code-scanning" 49 | uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 50 | with: 51 | sarif_file: results.sarif 52 | -------------------------------------------------------------------------------- /.github/workflows/update-deb-package-snapshots.yml: -------------------------------------------------------------------------------- 1 | name: update-snapshots 2 | on: 3 | # will send emails to last editor of this cron syntax (distroless-bot) 4 | schedule: 5 | - cron: "35 20 * * *" 6 | # allow this workflow to be manually run 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version: "1.20" 24 | 25 | - name: Update snapshots 26 | run: ./knife github-update-snapshots 27 | 28 | - name: Run update sboms script 29 | if: env.DISTROLESS_DIFF 30 | run: | 31 | for i in $(seq 5); do 32 | bazel --bazelrc=$GITHUB_WORKSPACE/.github/workflows/ci.bazelrc fetch //private/pkg/test/oci_image:test_sboms && break || sleep 20; 33 | done 34 | bazel run @//private/pkg/test/oci_image:test_sboms 35 | 36 | - name: Run update lockfile 37 | if: env.DISTROLESS_DIFF 38 | run: bazel mod deps --lockfile_mode=update 39 | 40 | - name: Create commits 41 | if: env.DISTROLESS_DIFF 42 | id: create-commits 43 | run: | 44 | git checkout -b update-snapshots 45 | 46 | # Set identity. 47 | git config --global user.email "distroless-bot@google.com" 48 | git config --global user.name "Distroless Bot" 49 | 50 | # Commit changes 51 | git add . 52 | git commit -s -m "Bumping packages to latest stable versions" 53 | git push --force origin HEAD 54 | 55 | - name: Create Pull Request 56 | if: env.DISTROLESS_DIFF 57 | env: 58 | GH_TOKEN: ${{ secrets.ACTIONS_TOKEN }} 59 | run: | 60 | BODY_FILE=$(mktemp) 61 | printf "Bumping packages to latest stable version\n\`\`\`diff\n$DISTROLESS_DIFF\n\`\`\`\n" >> $BODY_FILE 62 | if ! OUTPUT=$(gh pr create -B main -H update-snapshots -t "Bumping packages to latest stable versions" --body-file "$BODY_FILE" 2>&1) ; then 63 | echo $OUTPUT 64 | if [[ "${OUTPUT}" =~ "already exists" ]]; then 65 | echo "PR already exists and it was updated. Ending successfully"; 66 | exit 0; 67 | else 68 | exit 1; 69 | fi 70 | fi 71 | -------------------------------------------------------------------------------- /.github/workflows/update-node-archives.yml: -------------------------------------------------------------------------------- 1 | name: update-node-packages 2 | on: 3 | # will send emails to last editor of this cron syntax (distroless-bot) 4 | schedule: 5 | - cron: "35 20 * * *" 6 | # allow this workflow to be manually run 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | 23 | - name: Update node archives 24 | run: ./knife update-node-archives 25 | 26 | - name: Check for changes 27 | run: | 28 | git status 29 | if [[ $(git status --porcelain) ]]; then 30 | echo "DISTROLESS_DIFF=true" >> "$GITHUB_ENV" 31 | else 32 | echo "No changes detected" 33 | exit 0 34 | fi 35 | 36 | - name: Run update lockfile 37 | if: env.DISTROLESS_DIFF 38 | run: bazel mod deps --lockfile_mode=update 39 | 40 | - name: Create commits 41 | if: env.DISTROLESS_DIFF 42 | run: | 43 | git checkout -b update-node-archives 44 | 45 | # Set identity. 46 | git config --global user.email "distroless-bot@google.com" 47 | git config --global user.name "Distroless Bot" 48 | 49 | # Commit changes 50 | git add . 51 | git commit -s -m "Bumping node archives to latest stable versions" 52 | git push --force origin HEAD 53 | 54 | - name: Create Pull Request 55 | if: env.DISTROLESS_DIFF 56 | env: 57 | GH_TOKEN: ${{ secrets.ACTIONS_TOKEN }} 58 | run: | 59 | BODY_FILE=$(mktemp) 60 | if ! OUTPUT=$(gh pr create -B main -H update-node-archives --fill 2>&1) ; then 61 | echo $OUTPUT 62 | if [[ "${OUTPUT}" =~ "already exists" ]]; then 63 | echo "PR already exists and it was updated. Ending successfully"; 64 | exit 0; 65 | else 66 | exit 1; 67 | fi 68 | fi 69 | -------------------------------------------------------------------------------- /.github/workflows/update-temurin-packages.yml: -------------------------------------------------------------------------------- 1 | name: update-temurin-packages 2 | on: 3 | # will send emails to last editor of this cron syntax (distroless-bot) 4 | schedule: 5 | - cron: "35 20 * * *" 6 | # allow this workflow to be manually run 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Update snapshots 23 | run: ./knife update-java-archives 24 | 25 | - name: Check for changes 26 | run: | 27 | git status 28 | if [[ $(git status --porcelain) ]]; then 29 | echo "DISTROLESS_DIFF=true" >> "$GITHUB_ENV" 30 | else 31 | echo "No changes detected" 32 | exit 0 33 | fi 34 | 35 | - name: Run update lockfile 36 | if: env.DISTROLESS_DIFF 37 | run: bazel mod deps --lockfile_mode=update 38 | 39 | - name: Create commits 40 | if: env.DISTROLESS_DIFF 41 | run: | 42 | git checkout -b update-java-archives 43 | 44 | # Set identity. 45 | git config --global user.email "distroless-bot@google.com" 46 | git config --global user.name "Distroless Bot" 47 | 48 | # Commit changes 49 | git add . 50 | git commit -s -m "Bumping temurin archives to latest stable versions" 51 | git push --force origin HEAD 52 | 53 | - name: Create Pull Request 54 | if: env.DISTROLESS_DIFF 55 | env: 56 | GH_TOKEN: ${{ secrets.ACTIONS_TOKEN }} 57 | run: | 58 | BODY_FILE=$(mktemp) 59 | if ! OUTPUT=$(gh pr create -B main -H update-java-archives --fill 2>&1) ; then 60 | echo $OUTPUT 61 | if [[ "${OUTPUT}" =~ "already exists" ]]; then 62 | echo "PR already exists and it was updated. Ending successfully"; 63 | exit 0; 64 | else 65 | exit 1; 66 | fi 67 | fi 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore backup files. 2 | *~ 3 | # Ignore Vim swap files. 4 | .*.swp 5 | # Ignore files generated by IDEs. 6 | /.classpath 7 | /.factorypath 8 | .idea/ 9 | *.iml 10 | /.project 11 | /.settings 12 | /bazel.iml 13 | # Ignore all bazel-* symlinks. There is no full list since this can change 14 | # based on the name of the directory bazel is cloned into. 15 | /bazel-* 16 | # Ignore outputs generated during Bazel bootstrapping. 17 | /output/ 18 | # ignore user bazelrc 19 | .bazelrc.user 20 | *.log -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See CONTRIBUTING.md for instructions. 2 | # See https://pre-commit.com for more information 3 | # See https://pre-commit.com/hooks.html for more hooks 4 | 5 | # Commitizen runs in commit-msg stage 6 | # but we don't want to run the other hooks on commit messages 7 | default_stages: [commit] 8 | 9 | # Use a slightly older version of node by default 10 | # as the default uses a very new version of GLIBC 11 | default_language_version: 12 | node: 16.18.0 13 | 14 | repos: 15 | # Check formatting and lint for starlark code 16 | - repo: https://github.com/keith/pre-commit-buildifier 17 | rev: 6.1.0.1 18 | hooks: 19 | - id: buildifier 20 | - id: buildifier-lint 21 | # Enforce that commit messages allow for later changelog generation 22 | - repo: https://github.com/commitizen-tools/commitizen 23 | rev: v2.18.0 24 | hooks: 25 | # Requires that commitizen is already installed 26 | - id: commitizen 27 | stages: [commit-msg] 28 | - repo: https://github.com/pre-commit/mirrors-prettier 29 | rev: "v2.4.0" 30 | hooks: 31 | - id: prettier 32 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | private/pkg/test/oci_image/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | # Not technically required but suppresses 'Ruby' in Job status message. 3 | # This also lets us leverage GOARCH below. 4 | language: go 5 | jobs: 6 | include: 7 | - arch: amd64 8 | env: CPU=k8 9 | - arch: arm64 10 | env: CPU=aarch64 11 | 12 | install: 13 | - export PATH=$PATH:$HOME/bin && mkdir -p $HOME/bin 14 | - eval $(go env) 15 | # install bazelisk as bazel to install the appropriate bazel version 16 | - wget https://github.com/bazelbuild/bazelisk/releases/download/v1.6.1/bazelisk-linux-${GOARCH} && chmod +x bazelisk-linux-${GOARCH} && mv bazelisk-linux-${GOARCH} $HOME/bin/bazel 17 | 18 | script: 19 | - bazel clean --curses=no 20 | - bazel build --cpu=${CPU} --curses=no //package_manager:dpkg_parser.par 21 | - bazel build --cpu=${CPU} --curses=no //... 22 | # Build all targets tagged with our architecture: 23 | - bazel build --cpu=${CPU} --curses=no $(bazel query 'attr("tags", "'${GOARCH}'", "//...")') 24 | # Run all tests not tagged as "manual": 25 | - bazel test --cpu=${CPU} --curses=no --test_output=errors --test_timeout=900 //... 26 | # Run all tests tagged with our architecture: 27 | - bazel test --cpu=${CPU} --curses=no --test_output=errors --test_timeout=900 $(bazel query 'attr("tags", "'${GOARCH}'", "//...")') 28 | 29 | notifications: 30 | email: false 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles. 6 | 7 | Please fill out either the individual or corporate [Contributor License Agreement (CLA)](https://cla.developers.google.com/about). 8 | 9 | - If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](https://cla.developers.google.com/about/google-individual). 10 | - If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](https://cla.developers.google.com/about/google-corporate). 11 | 12 | Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. 13 | 14 | ## How to Build and Test 15 | 16 | 1. `bazel build //...` to build the whole project or ex:`bazel build //base:static_root_amd64_debian17` for a single image 17 | 18 | 2. For running tests, check `./knife test`. (`bazel test //...` will NOT run all tests, as many tests are marked "manual".) 19 | 20 | 3. For building and loading images to your local Docker engine, you need to add a new rule for that image to the BUILD: 21 | 22 | ```starlark 23 | load("@rules_oci//oci:defs.bzl", "oci_tarball") 24 | 25 | oci_tarball( 26 | name = "local_build", 27 | image = "//base:static_root_amd64_debian17", 28 | repo_tags = [], 29 | ) 30 | ``` 31 | 32 | then run the following command to load into the daemon 33 | 34 | ```shell 35 | bazel run //:local_build 36 | ``` 37 | 38 | ## Adding or removing Debian packages 39 | 40 | Whenever a change made to `common/*.yaml` manifests, the locking step has to be performed to regenerate lock files. 41 | 42 | This can be done by running; `./knife lock` 43 | 44 | ## Code style 45 | 46 | For styling Bazel files, install and run `buildifier` with: 47 | 48 | ```shell 49 | # Install buildifier version 3.2.0 50 | go install github.com/bazelbuild/buildtools/buildifier@latest 51 | 52 | # This will automatically fix files. 53 | buildifier -mode=fix $(find . -name 'BUILD*' -o -name 'WORKSPACE*' -o -name '*.bzl' -type f) 54 | ``` 55 | 56 | For styling Python files, [install](https://www.pylint.org/#install) and run `pylint` with: 57 | 58 | ```shell 59 | # Install pylint 60 | sudo pip install pylint 61 | # Or 62 | sudo apt-get install pylint 63 | # Or on macos 64 | brew install pylint 65 | 66 | # Identify python style issues. 67 | find . -name "*.py" | xargs pylint --disable=R,C 68 | ``` 69 | 70 | ## Contributing a Patch 71 | 72 | 1. Submit an issue describing your proposed change to the repo in question. 73 | 1. The repo owner will respond to your issue promptly. 74 | 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). 75 | 1. Fork the desired repo, develop and test your code changes. 76 | 1. Submit a pull request. 77 | -------------------------------------------------------------------------------- /MODULE.bazel: -------------------------------------------------------------------------------- 1 | "distroless" 2 | 3 | module(name = "distroless") 4 | 5 | bazel_dep(name = "bazel_skylib", version = "1.7.1") 6 | bazel_dep(name = "aspect_bazel_lib", version = "2.7.9") 7 | bazel_dep(name = "platforms", version = "0.0.10") 8 | bazel_dep(name = "rules_go", version = "0.47.0") 9 | bazel_dep(name = "gazelle", version = "0.38.0") 10 | bazel_dep(name = "rules_pkg", version = "1.0.1") 11 | bazel_dep(name = "rules_rust", version = "0.49.1") 12 | bazel_dep(name = "container_structure_test", version = "1.16.0") 13 | bazel_dep(name = "rules_oci", version = "1.7.5") 14 | bazel_dep(name = "rules_distroless", version = "0.3.8") 15 | bazel_dep(name = "rules_python", version = "0.35.0") 16 | 17 | ### OCI ### 18 | oci = use_extension("@rules_oci//oci:extensions.bzl", "oci") 19 | oci.toolchains(crane_version = "v0.18.0") 20 | use_repo(oci, "oci_crane_toolchains") 21 | 22 | ### PYTHON ### 23 | python = use_extension("@rules_python//python/extensions:python.bzl", "python") 24 | python.toolchain( 25 | ignore_root_user_error = True, 26 | python_version = "3.11", 27 | ) 28 | 29 | ### GO #### 30 | go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") 31 | go_deps.module( 32 | path = "github.com/spdx/tools-golang", 33 | sum = "h1:9B623Cfs+mclYK6dsae7gLSwuIBHvlgmEup87qpqsAQ=", 34 | version = "v0.3.1-0.20230104082527-d6f58551be3f", 35 | ) 36 | use_repo(go_deps, "com_github_spdx_tools_golang") 37 | 38 | ### JETTY ### 39 | http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 40 | 41 | http_archive( 42 | name = "jetty", 43 | add_prefix = "output", 44 | build_file = "//java:BUILD.jetty", 45 | sha256 = "b04b4cd45f3bf3c09a26bdf7f4e8d1a67e1a0f224ef4539534a0719b2c701088", 46 | strip_prefix = "jetty-distribution-9.4.53.v20231009/", 47 | urls = ["https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-distribution/9.4.53.v20231009/jetty-distribution-9.4.53.v20231009.tar.gz"], 48 | ) 49 | 50 | ### BUSYBOX ### 51 | busybox = use_extension("//private/extensions:busybox.bzl", "busybox") 52 | busybox.archive() 53 | use_repo(busybox, "busybox_amd64", "busybox_arm", "busybox_arm64", "busybox_ppc64le", "busybox_s390x") 54 | 55 | ### JAVA ### 56 | include("//private/repos:java.MODULE.bazel") 57 | 58 | ### NODE ### 59 | node = use_extension("//private/extensions:node.bzl", "node") 60 | node.archive() 61 | use_repo(node, "nodejs20_amd64", "nodejs20_arm", "nodejs20_arm64", "nodejs20_ppc64le", "nodejs20_s390x", "nodejs22_amd64", "nodejs22_arm", "nodejs22_arm64", "nodejs22_ppc64le", "nodejs22_s390x", "nodejs24_amd64", "nodejs24_arm64", "nodejs24_ppc64le", "nodejs24_s390x") 62 | 63 | ### DEBIAN ### 64 | include("//private/repos/deb:deb.MODULE.bazel") 65 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Images in this repository are built and released in the `gcr.io/distroless` repository on the [Google Container Registry](https://cloud.google.com/container-registry/). 4 | 5 | Images are automatically built and pushed every commit, according to the policy defined in [cloudbuild.yaml](.cloudbuild/cloudbuild.yaml). 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Distroless currently tracks debian 12 ([bookworm](https://packages.debian.org/bookworm)) packages. 6 | 7 | Debian package versions used for the current build are found in https://github.com/GoogleContainerTools/distroless/blob/main/private/repos/deb. It can be parsed and printed into simple json data by invoking `./knife deb-versions` at the root of this project. 8 | 9 | ## Reporting a Vulnerability 10 | 11 | If a distroless image you are using contains a CVE or other vulnerability: 12 | 1. ensure you are using a [currently supported image](https://github.com/GoogleContainerTools/distroless#what-images-are-available) 13 | 1. find the appropriate debian security-tracker notice: `https://security-tracker.debian.org/tracker/CVE-XXXX-YYYYY`, for [example](https://security-tracker.debian.org/tracker/CVE-2022-21476). 14 | 1. check if a fix is available for the appropriate debian version in the main/security channels (ex `bookworm`, `bookworm (security)`). 15 | 1. if a fix is not yet available, do not file a bug, track it in your internal tracker until one becomes available. 16 | 1. if a fix is available *and* it has been more than 48 hours, please let the team know by creating an issue and pointing to the CVE or vulnerability disclosure. 17 | -------------------------------------------------------------------------------- /SUPPORT_POLICY.md: -------------------------------------------------------------------------------- 1 | Distroless currently tracks Debian on their [standard support timeline](https://wiki.debian.org/DebianReleases#Production_Releases). 2 | 3 | The current estimation of end of life for images with the pattern: 4 | 5 | `gcr.io/distroless/-debian:(latest|nonroot|debug|debug-nonroot)` 6 | 7 | | Image | Debian 12 EOL | 8 | | ----------- | --------------------------- | 9 | | static | debian 13 release day + 1yr | 10 | | base | debian 13 release day + 1yr | 11 | | base-nossl | debian 13 release day + 1yr | 12 | | cc | debian 13 release day + 1yr | 13 | | java* | debian 13 release day + 3mo | 14 | | node* | debian 13 release day + 3mo | 15 | | python* | debian 13 release day + 3mo | 16 | 17 | \* see below for language specific runtime notes 18 | 19 | 20 | ### Java 21 | Java will only support current LTS version distributed by debian [see here](https://wiki.debian.org/Java). 22 | 23 | ### Node 24 | Node version support is for even numbered releases (20, 22, 24, etc) that are current, active or in LTS maintenance. For more information, [see here](https://nodejs.org/en/about/previous-releases#release-schedule). 25 | 26 | ### Images no longer supported (TBD) 27 | A list of supported image tags is available here: https://github.com/GoogleContainerTools/distroless#what-images-are-available 28 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@rules_oci//cosign:repositories.bzl", "cosign_register_toolchains") 2 | 3 | cosign_register_toolchains(name = "oci_cosign") 4 | -------------------------------------------------------------------------------- /base/BUILD: -------------------------------------------------------------------------------- 1 | load(":base.bzl", "base_images") 2 | load(":distro.bzl", "DISTROS") 3 | 4 | package(default_visibility = ["//visibility:public"]) 5 | 6 | [ 7 | base_images(distro = distro) 8 | for distro in DISTROS 9 | ] 10 | -------------------------------------------------------------------------------- /base/README.md: -------------------------------------------------------------------------------- 1 | # Documentation for `gcr.io/distroless/base`, `gcr.io/distroless/base-nossl` and `gcr.io/distroless/static` 2 | 3 | ## Image Contents 4 | 5 | This image contains a minimal Linux, glibc-based system. It is intended for use directly by "mostly-statically compiled" languages like Go, Rust or D. 6 | 7 | Statically compiled applications (Go) that do not require libc can use the `gcr.io/distroless/static` image, which contains: 8 | 9 | * ca-certificates 10 | * A /etc/passwd entry for a root user 11 | * A /tmp directory 12 | * tzdata 13 | 14 | Applications that require libc but do not need libssl can use the `gcr.io/distroless/base-nossl`, which contains all 15 | of the packages in `gcr.io/distroless/static`, and 16 | 17 | * glibc 18 | 19 | Most other applications (and Go apps that require libc/cgo) should start with `gcr.io/distroless/base`, which contains all 20 | of the packages in `gcr.io/distroless/static`, and 21 | 22 | * glibc 23 | * libssl 24 | 25 | ## Usage 26 | 27 | Users are expected to include their compiled application and set the correct cmd in their image. 28 | -------------------------------------------------------------------------------- /base/distro.bzl: -------------------------------------------------------------------------------- 1 | DISTROS = ["debian12"] 2 | -------------------------------------------------------------------------------- /base/test.sh: -------------------------------------------------------------------------------- 1 | $RUNFILES_DIR/runtimes_common/structure_tests/ext_run.sh \ 2 | -i bazel/base:cc \ 3 | -t $RUNFILES_DIR/distroless/base/base.tar \ 4 | -c $RUNFILES_DIR/distroless/base/testdata/base.yaml 5 | -------------------------------------------------------------------------------- /base/testdata/base.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | fileExistenceTests: 3 | # Basic FS sanity checks. 4 | - name: root 5 | path: '/' 6 | shouldExist: true 7 | - name: tmp 8 | path: '/tmp' 9 | shouldExist: true 10 | - name: passwd 11 | path: '/etc/passwd' 12 | shouldExist: true 13 | - name: group 14 | path: '/etc/group' 15 | shouldExist: true 16 | - name: etc-os-release 17 | path: '/etc/os-release' 18 | shouldExist: true 19 | - name: certs 20 | path: '/etc/ssl/certs/ca-certificates.crt' 21 | shouldExist: true 22 | - name: certs_copyright 23 | path: '/usr/share/doc/ca-certificates/copyright' 24 | shouldExist: true 25 | - name: services 26 | path: '/etc/services' 27 | shouldExist: true 28 | - name: tzdata_copyright 29 | path: '/usr/share/doc/tzdata/copyright' 30 | shouldExist: true 31 | - name: tzdata_zoneinfo 32 | path: '/usr/share/zoneinfo' 33 | shouldExist: true 34 | - name: homedir 35 | path: '/root' 36 | shouldExist: true 37 | - name: nonroot-homedir 38 | path: '/home/nonroot' 39 | shouldExist: true 40 | - name: dpkg-status.d 41 | path: '/var/lib/dpkg/status.d/libc6' 42 | shouldExist: true 43 | fileContentTests: 44 | - name: 'known users' 45 | path: '/etc/passwd' 46 | expectedContents: ['^root:x:0:0:root:/root:/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/sbin/nologin\nnonroot:x:65532:65532:nonroot:/home/nonroot:/sbin/nologin\n$'] 47 | - name: 'known groups' 48 | path: '/etc/group' 49 | expectedContents: ['^root:x:0:\nnobody:x:65534:\ntty:x:5:\nstaff:x:50:\nnonroot:x:65532:\n$'] 50 | - name: '/usr/lib/os-release pretty name' 51 | path: '/usr/lib/os-release' 52 | expectedContents: ['PRETTY_NAME="Distroless"'] 53 | # /etc/os-release is a symlink to /usr/lib/os-release, make sure they match. 54 | - name: '/etc/os-release pretty name' 55 | path: '/etc/os-release' 56 | expectedContents: ['PRETTY_NAME="Distroless"'] 57 | -------------------------------------------------------------------------------- /base/testdata/certs.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | # Check that libssl finds the certificates 4 | - name: openssl verify google 5 | command: ["openssl", "s_client", "-strict", "-verify_return_error", "-connect", "www.google.com:443"] 6 | exitCode: 0 7 | expectedOutput: ["Verification: OK"] 8 | -------------------------------------------------------------------------------- /base/testdata/check_certs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "crypto/x509" 19 | "fmt" 20 | "os" 21 | ) 22 | 23 | func main() { 24 | pool, err := x509.SystemCertPool() 25 | if err != nil { 26 | fmt.Printf("Error %s loading system certs.\n", err) 27 | os.Exit(1) 28 | } 29 | if pool == nil { 30 | fmt.Println("No cert pools.") 31 | os.Exit(1) 32 | } 33 | fmt.Println("Certs working!") 34 | } 35 | -------------------------------------------------------------------------------- /base/testdata/debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | fileContentTests: 3 | - name: 'os-release contents' 4 | path: '/etc/os-release' 5 | expectedContents: ['.*\nVERSION="Debian GNU/Linux 12 \(bookworm\)"\n'] 6 | -------------------------------------------------------------------------------- /base/testdata/debug.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | fileExistenceTests: 3 | # Basic FS sanity checks. 4 | - name: busybox 5 | path: '/busybox' 6 | shouldExist: true 7 | commandTests: 8 | - name: busybox 9 | command: ["/busybox/busybox"] 10 | expectedOutput: ['BusyBox v1\.36\.1'] 11 | -------------------------------------------------------------------------------- /base/testdata/static.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | # Check that Go programs can read the certificates 4 | - name: certs 5 | command: ["/check_certs"] 6 | exitCode: 0 7 | -------------------------------------------------------------------------------- /cc/BUILD: -------------------------------------------------------------------------------- 1 | load("@rules_oci//oci:defs.bzl", "oci_image", "oci_image_index") 2 | load("//:checksums.bzl", "ARCHITECTURES") 3 | load("//base:distro.bzl", "DISTROS") 4 | load("//private/util:deb.bzl", "deb") 5 | 6 | package(default_visibility = ["//visibility:public"]) 7 | 8 | # An image for C/C++ programs 9 | [ 10 | oci_image_index( 11 | name = ("cc" if not mode else mode[1:]) + "_" + user + "_" + distro, 12 | images = [ 13 | ("cc" if not mode else mode[1:]) + "_" + user + "_" + arch + "_" + distro 14 | for arch in ARCHITECTURES 15 | ], 16 | ) 17 | for mode in [ 18 | "", 19 | ":debug", 20 | ] 21 | for user in [ 22 | "root", 23 | "nonroot", 24 | ] 25 | for distro in DISTROS 26 | ] 27 | 28 | [ 29 | oci_image( 30 | name = ("cc" if (not mode) else mode[1:]) + "_" + user + "_" + arch + "_" + distro, 31 | base = "//base" + (mode if mode else ":base") + "_" + user + "_" + arch + "_" + distro, 32 | tars = [ 33 | deb.package(arch, distro, "gcc-12-base"), 34 | deb.package(arch, distro, "libgomp1"), 35 | deb.package(arch, distro, "libstdc++6"), 36 | deb.package(arch, distro, "libgcc-s1"), 37 | ], 38 | ) 39 | for mode in [ 40 | "", 41 | ":debug", 42 | ] 43 | for arch in ARCHITECTURES 44 | for user in [ 45 | "root", 46 | "nonroot", 47 | ] 48 | for distro in DISTROS 49 | ] 50 | -------------------------------------------------------------------------------- /cc/README.md: -------------------------------------------------------------------------------- 1 | # Documentation for `gcr.io/distroless/cc` 2 | 3 | ## Image Contents 4 | 5 | This image contains a minimal Linux, glibc runtime for "mostly-statically compiled" languages like Rust and D. 6 | 7 | Specifically, the image contains everything in the [base image](../base/README.md), plus: 8 | 9 | * libgcc1 and its dependencies. 10 | 11 | ## Usage 12 | 13 | Users are expected to include their compiled application and set the correct CMD in their image. 14 | -------------------------------------------------------------------------------- /checksums.bzl: -------------------------------------------------------------------------------- 1 | BASE_ARCHITECTURES = ["amd64", "arm64"] 2 | 3 | ARCHITECTURES = BASE_ARCHITECTURES + ["arm", "s390x", "ppc64le"] 4 | 5 | VERSIONS = [ 6 | ("debian11", "bullseye", "11"), # deprecated 7 | ("debian12", "bookworm", "12"), 8 | ] 9 | 10 | VARIANTS = { 11 | "arm": "v7", 12 | "arm64": "v8", 13 | } 14 | -------------------------------------------------------------------------------- /common/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@aspect_bazel_lib//lib:tar.bzl", "tar") 2 | load("@rules_distroless//distroless:defs.bzl", "cacerts", "group", "home", "locale", "os_release", "passwd") 3 | load("//:checksums.bzl", "ARCHITECTURES", "VERSIONS") 4 | load("//base:distro.bzl", "DISTROS") 5 | load("//private/util:deb.bzl", "deb") 6 | load(":variables.bzl", "MTIME", "NOBODY", "NONROOT", "OS_RELEASE", "ROOT", "quote") 7 | 8 | package(default_visibility = ["//visibility:public"]) 9 | 10 | tar( 11 | name = "rootfs", 12 | srcs = [], 13 | args = [ 14 | "--format", 15 | "gnutar", 16 | ], 17 | compress = "gzip", 18 | mtree = ["./ type=dir uid=0 gid=0 time=0.0"], 19 | ) 20 | 21 | tar( 22 | name = "tmp", 23 | srcs = [], 24 | # original tmp.tar was created on a gnutar, mimic that. 25 | args = [ 26 | "--format", 27 | "gnutar", 28 | ], 29 | compress = "gzip", 30 | mtree = ["./tmp gname=root uname=root time=1501783453.0 mode=1777 gid=0 uid=0 type=dir"], 31 | ) 32 | 33 | [ 34 | os_release( 35 | name = "os_release_%s" % dist, 36 | content = { 37 | key: quote(value.format( 38 | CODENAME = codename, 39 | VERSION = version, 40 | )) 41 | for (key, value) in OS_RELEASE.items() 42 | }, 43 | time = MTIME, 44 | ) 45 | for (dist, codename, version) in VERSIONS 46 | ] 47 | 48 | [ 49 | locale( 50 | name = "locale_%s_%s" % (distro, arch), 51 | charset = "C.utf8", 52 | package = deb.data(arch, distro, "libc-bin"), 53 | time = MTIME, 54 | ) 55 | for arch in ARCHITECTURES 56 | for distro in DISTROS 57 | ] 58 | 59 | [ 60 | cacerts( 61 | name = "cacerts_%s_%s" % (distro, arch), 62 | package = deb.data(arch, distro, "ca-certificates"), 63 | time = MTIME, 64 | ) 65 | for arch in ARCHITECTURES 66 | for distro in DISTROS 67 | ] 68 | 69 | # create /etc/group with the root, tty, and staff groups 70 | group( 71 | name = "group", 72 | entries = [ 73 | { 74 | "name": "root", # root_group 75 | "gid": ROOT, 76 | "password": "x", 77 | }, 78 | { 79 | "name": "nobody", # nobody_group 80 | "gid": NOBODY, 81 | "password": "x", 82 | }, 83 | { 84 | "name": "tty", # tty_group 85 | "gid": 5, 86 | "password": "x", 87 | }, 88 | { 89 | "name": "staff", # staff_group 90 | "gid": 50, 91 | "password": "x", 92 | }, 93 | { 94 | "name": "nonroot", # nonroot_group 95 | "gid": NONROOT, 96 | "password": "x", 97 | }, 98 | ], 99 | time = MTIME, 100 | ) 101 | 102 | passwd( 103 | name = "passwd", 104 | entries = [ 105 | { 106 | "gecos": ["root"], 107 | "gid": ROOT, 108 | "shell": "/sbin/nologin", 109 | "home": "/root", 110 | "uid": ROOT, 111 | "password": "x", 112 | "username": "root", 113 | }, 114 | { 115 | "gecos": ["nobody"], 116 | "gid": NOBODY, 117 | "home": "/nonexistent", 118 | "shell": "/sbin/nologin", 119 | "uid": NOBODY, 120 | "password": "x", 121 | "username": "nobody", 122 | }, 123 | { 124 | "gecos": ["nonroot"], 125 | "gid": NONROOT, 126 | "home": "/home/nonroot", 127 | "shell": "/sbin/nologin", 128 | "uid": NONROOT, 129 | "password": "x", 130 | "username": "nonroot", 131 | }, 132 | ], 133 | ) 134 | 135 | home( 136 | name = "home", 137 | dirs = [ 138 | { 139 | "home": "/root", 140 | "uid": ROOT, 141 | "gid": ROOT, 142 | "mode": 700, 143 | }, 144 | { 145 | "home": "/home", 146 | "uid": ROOT, 147 | "gid": ROOT, 148 | "mode": 755, 149 | }, 150 | { 151 | "home": "/home/nonroot", 152 | "uid": NONROOT, 153 | "gid": NONROOT, 154 | "mode": 700, 155 | }, 156 | ], 157 | ) 158 | -------------------------------------------------------------------------------- /common/variables.bzl: -------------------------------------------------------------------------------- 1 | "common variables" 2 | 3 | def quote(str): 4 | return '''"{}"'''.format(str) 5 | 6 | OS_RELEASE = dict( 7 | PRETTY_NAME = "Distroless", 8 | NAME = "Debian GNU/Linux", 9 | ID = "debian", 10 | VERSION_ID = "{VERSION}", 11 | VERSION = "Debian GNU/Linux {VERSION} ({CODENAME})", 12 | HOME_URL = "https://github.com/GoogleContainerTools/distroless", 13 | SUPPORT_URL = "https://github.com/GoogleContainerTools/distroless/blob/master/README.md", 14 | BUG_REPORT_URL = "https://github.com/GoogleContainerTools/distroless/issues/new", 15 | ) 16 | 17 | NOBODY = 65534 18 | NONROOT = 65532 19 | ROOT = 0 20 | 21 | # TODO: this should be 0, but for now we'll use this to minimize diff. 22 | MTIME = "946684800" 23 | -------------------------------------------------------------------------------- /cosign.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWZzVzkb8A+DbgDpaJId/bOmV8n7Q 3 | OqxYbK0Iro6GzSmOzxkn+N2AKawLyXi84WSwJQBK//psATakCgAQKkNTAA== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /examples/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | -------------------------------------------------------------------------------- /examples/cc/BUILD: -------------------------------------------------------------------------------- 1 | # Public notice: this file is for internal documentation, testing, and 2 | # reference only. Note that repo maintainers can freely change any part of the 3 | # repository code at any time. 4 | load("@container_structure_test//:defs.bzl", "container_structure_test") 5 | load("//base:distro.bzl", "DISTROS") 6 | load("//private/oci:defs.bzl", "cc_image") 7 | 8 | package(default_visibility = ["//visibility:public"]) 9 | 10 | [cc_image( 11 | name = "hello_" + distro, 12 | srcs = ["hello.c"], 13 | base = "//cc:cc_root_amd64_" + distro, 14 | ) for distro in DISTROS] 15 | 16 | [cc_image( 17 | name = "hello_cc_" + distro, 18 | srcs = ["hello_cc.cc"], 19 | base = "//cc:cc_root_amd64_" + distro, 20 | ) for distro in DISTROS] 21 | 22 | [container_structure_test( 23 | name = "hello_" + distro + "_test", 24 | size = "small", 25 | configs = ["testdata/hello_" + distro + ".yaml"], 26 | image = ":hello_" + distro, 27 | tags = [ 28 | "amd64", 29 | "manual", 30 | ], 31 | ) for distro in DISTROS] 32 | 33 | [container_structure_test( 34 | name = "hello_cc_" + distro + "_test", 35 | size = "small", 36 | configs = ["testdata/hello_cc_" + distro + ".yaml"], 37 | image = ":hello_cc_" + distro, 38 | tags = [ 39 | "amd64", 40 | "manual", 41 | ], 42 | ) for distro in DISTROS] 43 | -------------------------------------------------------------------------------- /examples/cc/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcc:6 AS build-env 2 | COPY . /app 3 | WORKDIR /app 4 | RUN cc hello.c -o hello 5 | 6 | FROM gcr.io/distroless/cc 7 | COPY --from=build-env /app /app 8 | WORKDIR /app 9 | CMD ["./hello"] 10 | -------------------------------------------------------------------------------- /examples/cc/hello.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("Hello from distroless C!\n"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /examples/cc/hello_cc.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | std::cout << "Hello from distroless C++!" << std::endl; 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /examples/cc/testdata/hello_cc_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: hello_cc 4 | command: ['/hello_cc_debian12_binary'] 5 | expectedOutput: ['Hello from distroless C\+\+!'] 6 | -------------------------------------------------------------------------------- /examples/cc/testdata/hello_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: hello 4 | command: ['/hello_debian12_binary'] 5 | expectedOutput: ['Hello from distroless C!'] 6 | -------------------------------------------------------------------------------- /examples/go/BUILD: -------------------------------------------------------------------------------- 1 | # Public notice: this file is for internal documentation, testing, and 2 | # reference only. Note that repo maintainers can freely change any part of the 3 | # repository code at any time. 4 | load("@rules_oci//oci:defs.bzl", "oci_tarball") 5 | load("//private/oci:defs.bzl", "go_image") 6 | 7 | package(default_visibility = ["//visibility:public"]) 8 | 9 | go_image( 10 | name = "go_example", 11 | srcs = ["main.go"], 12 | base = "//base:base_root_amd64_debian12", 13 | ) 14 | 15 | # Run 16 | # bazel build //examples/go:tarball 17 | # podman load -i bazel-bin/examples/go/tarball/tarball.tar 18 | # podman run localhost/distroless/examples/go:latest 19 | oci_tarball( 20 | name = "tarball", 21 | image = ":go_example", 22 | repo_tags = ["distroless/examples/go:latest"], 23 | ) 24 | -------------------------------------------------------------------------------- /examples/go/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 as build 2 | 3 | WORKDIR /go/src/app 4 | COPY . . 5 | 6 | RUN go mod download 7 | RUN go vet -v 8 | RUN go test -v 9 | 10 | RUN CGO_ENABLED=0 go build -o /go/bin/app 11 | 12 | FROM gcr.io/distroless/static-debian12 13 | 14 | COPY --from=build /go/bin/app / 15 | CMD ["/app"] 16 | -------------------------------------------------------------------------------- /examples/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleContainerTools/distroless/examples/go 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /examples/go/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import "fmt" 18 | 19 | func main() { 20 | fmt.Println("Hello, world!") 21 | } 22 | -------------------------------------------------------------------------------- /examples/go/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test1(t *testing.T) { 8 | } 9 | -------------------------------------------------------------------------------- /examples/java/BUILD: -------------------------------------------------------------------------------- 1 | # Public notice: this file is for internal documentation, testing, and 2 | # reference only. Note that repo maintainers can freely change any part of the 3 | # repository code at any time. 4 | load("@container_structure_test//:defs.bzl", "container_structure_test") 5 | load("//private/oci:defs.bzl", "java_image") 6 | 7 | package(default_visibility = ["//visibility:public"]) 8 | 9 | JAVA_VERSIONS_PER_DISTRO = [ 10 | ("17", "debian12"), 11 | ] 12 | 13 | [ 14 | java_image( 15 | name = "hello_java" + java_version + "_" + user + "_" + distro, 16 | srcs = ["HelloJava.java"], 17 | base = "//java:java" + java_version + "_" + user + "_amd64" + "_" + distro, 18 | main_class = "examples.HelloJava", 19 | ) 20 | for user in [ 21 | "root", 22 | "nonroot", 23 | ] 24 | for java_version, distro in JAVA_VERSIONS_PER_DISTRO 25 | ] 26 | 27 | [ 28 | container_structure_test( 29 | name = "hello_java" + java_version + "_" + user + "_" + distro + "_test", 30 | size = "small", 31 | configs = ["testdata/hello_" + user + "_" + distro + ".yaml"], 32 | image = ":hello_java" + java_version + "_" + user + "_" + distro, 33 | tags = [ 34 | "amd64", 35 | "manual", 36 | ], 37 | ) 38 | for user in [ 39 | "root", 40 | "nonroot", 41 | ] 42 | for java_version, distro in JAVA_VERSIONS_PER_DISTRO 43 | ] 44 | -------------------------------------------------------------------------------- /examples/java/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17-jdk-slim AS build-env 2 | COPY . /app/examples 3 | WORKDIR /app 4 | RUN javac examples/*.java 5 | RUN jar cfe main.jar examples.HelloJava examples/*.class 6 | 7 | FROM gcr.io/distroless/java17-debian12 8 | COPY --from=build-env /app /app 9 | WORKDIR /app 10 | CMD ["main.jar"] 11 | -------------------------------------------------------------------------------- /examples/java/HelloJava.java: -------------------------------------------------------------------------------- 1 | package examples; 2 | 3 | public class HelloJava { 4 | public static void main(String[] args) { 5 | System.out.println("Hello world"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/java/testdata/hello_nonroot_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: hello 4 | command: ['/usr/bin/java', '-cp', '/*', 'examples.HelloJava'] 5 | expectedOutput: ['Hello world'] 6 | -------------------------------------------------------------------------------- /examples/java/testdata/hello_root_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: hello 4 | command: ['/usr/bin/java', '-cp', '/*', 'examples.HelloJava'] 5 | expectedOutput: ['Hello world'] 6 | -------------------------------------------------------------------------------- /examples/nodejs/BUILD: -------------------------------------------------------------------------------- 1 | # Public notice: this file is for internal documentation, testing, and 2 | # reference only. Note that repo maintainers can freely change any part of the 3 | # repository code at any time. 4 | load("@container_structure_test//:defs.bzl", "container_structure_test") 5 | load("@rules_oci//oci:defs.bzl", "oci_image") 6 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 7 | load("//:checksums.bzl", ARCHITECTURES = "BASE_ARCHITECTURES") 8 | load("//base:distro.bzl", "DISTROS") 9 | 10 | package(default_visibility = ["//visibility:public"]) 11 | 12 | # These examples are adapted from: 13 | # https://howtonode.org/hello-node 14 | 15 | pkg_tar( 16 | name = "hello_tar", 17 | srcs = [ 18 | "hello.js", 19 | ], 20 | ) 21 | 22 | pkg_tar( 23 | name = "hello_http_tar", 24 | srcs = [ 25 | "hello_http.js", 26 | ], 27 | ) 28 | 29 | [ 30 | oci_image( 31 | name = "hello_" + user + "_" + arch + "_" + distro, 32 | base = "//nodejs:nodejs22_" + user + "_" + arch + "_" + distro, 33 | cmd = ["hello.js"], 34 | tars = [":hello_tar"], 35 | ) 36 | for user in [ 37 | "root", 38 | "nonroot", 39 | ] 40 | for arch in ARCHITECTURES 41 | for distro in DISTROS 42 | ] 43 | 44 | [ 45 | oci_image( 46 | name = "hello_http_" + user + "_" + arch + "_" + distro, 47 | base = "//nodejs:nodejs22_" + user + "_" + arch + "_" + distro, 48 | cmd = ["hello_http.js"], 49 | tars = [":hello_http_tar"], 50 | ) 51 | for user in [ 52 | "root", 53 | "nonroot", 54 | ] 55 | for arch in ARCHITECTURES 56 | for distro in DISTROS 57 | ] 58 | 59 | [ 60 | container_structure_test( 61 | name = "hello_" + user + "_" + arch + "_" + distro + "_test", 62 | configs = ["testdata/hello.yaml"], 63 | image = ":hello_" + user + "_" + arch + "_" + distro, 64 | tags = [ 65 | arch, 66 | "manual", 67 | ], 68 | ) 69 | for user in [ 70 | "root", 71 | "nonroot", 72 | ] 73 | for arch in ARCHITECTURES 74 | for distro in DISTROS 75 | ] 76 | -------------------------------------------------------------------------------- /examples/nodejs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS build-env 2 | COPY . /app 3 | WORKDIR /app 4 | 5 | RUN npm ci --omit=dev 6 | 7 | FROM gcr.io/distroless/nodejs22-debian12 8 | COPY --from=build-env /app /app 9 | WORKDIR /app 10 | CMD ["hello.js"] 11 | -------------------------------------------------------------------------------- /examples/nodejs/hello.js: -------------------------------------------------------------------------------- 1 | 2 | console.log("Hello World"); 3 | -------------------------------------------------------------------------------- /examples/nodejs/hello_http.js: -------------------------------------------------------------------------------- 1 | // Load the http module to create an http server. 2 | var http = require('http'); 3 | 4 | // Configure our HTTP server to respond with Hello World to all requests. 5 | var server = http.createServer(function (request, response) { 6 | response.writeHead(200, {"Content-Type": "text/plain"}); 7 | response.end("Hello World\n"); 8 | }); 9 | 10 | // Listen on port 8000, IP defaults to 127.0.0.1 11 | server.listen(8000); 12 | 13 | // Put a friendly message on the terminal 14 | console.log("Server running at http://127.0.0.1:8000/"); 15 | -------------------------------------------------------------------------------- /examples/nodejs/node-express/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS build-env 2 | ADD . /app 3 | WORKDIR /app 4 | RUN npm install --omit=dev 5 | 6 | FROM gcr.io/distroless/nodejs22-debian12 7 | COPY --from=build-env /app /app 8 | WORKDIR /app 9 | EXPOSE 3000 10 | CMD ["hello_express.js"] 11 | -------------------------------------------------------------------------------- /examples/nodejs/node-express/hello_express.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const port = 3000 4 | 5 | app.get('/', (req, res) => res.send('Hello World!')) 6 | 7 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 8 | -------------------------------------------------------------------------------- /examples/nodejs/node-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "distroless-express", 3 | "version": "1.0.0", 4 | "description": "Distroless express node.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/GoogleContainerTools/distroless.git" 8 | }, 9 | "dependencies": { 10 | "express": "4.18.1" 11 | }, 12 | "author": "Bryant Hagadorn", 13 | "license": "ISC" 14 | } -------------------------------------------------------------------------------- /examples/nodejs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "nodejs", 9 | "version": "1.0.0", 10 | "license": "ISC" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "description": "Example", 5 | "main": "hello.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /examples/nodejs/testdata/hello.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: hello 4 | command: ["/nodejs/bin/node", "/hello.js"] 5 | expectedOutput: ['Hello World'] 6 | -------------------------------------------------------------------------------- /examples/nonroot/BUILD: -------------------------------------------------------------------------------- 1 | # Public notice: this file is for internal documentation, testing, and 2 | # reference only. Note that repo maintainers can freely change any part of the 3 | # repository code at any time. 4 | load("@container_structure_test//:defs.bzl", "container_structure_test") 5 | load("@rules_go//go:def.bzl", "go_binary") 6 | load("@rules_distroless//distroless:defs.bzl", "home", "passwd") 7 | load("@rules_oci//oci:defs.bzl", "oci_image") 8 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 9 | load("//base:distro.bzl", "DISTROS") 10 | 11 | # Create a passwd file and home directory with a nonroot user and uid. 12 | passwd( 13 | name = "passwd", 14 | entries = [ 15 | { 16 | "gecos": ["nonroot"], 17 | "gid": 1000, 18 | "home": "/home", 19 | "shell": "/bin/bash", 20 | "uid": 1002, 21 | "username": "nonroot", 22 | }, 23 | ], 24 | ) 25 | 26 | home( 27 | name = "home", 28 | dirs = [ 29 | { 30 | "home": "/home", 31 | "uid": 1002, 32 | "gid": 1000, 33 | }, 34 | ], 35 | ) 36 | 37 | # Include it in our image as a tar. 38 | oci_image( 39 | name = "passwd_image", 40 | base = "//base:base_root_amd64_debian12", 41 | tars = [ 42 | ":passwd", 43 | ":home", 44 | ], 45 | user = "nonroot", 46 | visibility = ["//visibility:private"], 47 | ) 48 | 49 | # Simple go program to print out the username and uid. 50 | go_binary( 51 | name = "user", 52 | srcs = ["testdata/user.go"], 53 | goarch = "amd64", 54 | # Test image is linux based 55 | goos = "linux", 56 | pure = "on", 57 | ) 58 | 59 | pkg_tar( 60 | name = "user_tar", 61 | srcs = [":user"], 62 | ) 63 | 64 | [oci_image( 65 | name = "check_user_image_" + distro, 66 | base = ":passwd_image", 67 | tars = [":user_tar"], 68 | visibility = ["//visibility:private"], 69 | ) for distro in DISTROS] 70 | 71 | # Test to verify this works :) 72 | [container_structure_test( 73 | name = "check_user_" + distro + "_test", 74 | configs = ["testdata/user.yaml"], 75 | image = ":check_user_image_" + distro, 76 | tags = [ 77 | "amd64", 78 | "manual", 79 | ], 80 | visibility = ["//visibility:private"], 81 | ) for distro in DISTROS] 82 | -------------------------------------------------------------------------------- /examples/nonroot/testdata/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All rights reserved. 2 | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "os" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | func main() { 26 | // We can't use user.Current here because it requires cgo. 27 | uid := strconv.Itoa(os.Getuid()) 28 | 29 | passwd, err := ioutil.ReadFile("/etc/passwd") 30 | if err != nil { 31 | fmt.Printf("Error reading /etc/passwd: %v", err) 32 | return 33 | } 34 | for _, line := range strings.Split(string(passwd), "\n") { 35 | if len(line) == 0 { 36 | continue 37 | } 38 | entries := strings.Split(line, ":") 39 | name, id := entries[0], entries[2] 40 | if id == uid { 41 | fmt.Println("User:", name) 42 | fmt.Println("Uid:", id) 43 | return 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/nonroot/testdata/user.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: user 4 | command: ['/user'] 5 | expectedOutput: ['User: nonroot', 'Uid: 1002'] 6 | -------------------------------------------------------------------------------- /examples/python3-requirements/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build a virtualenv using the appropriate Debian release 2 | # * Install python3-venv for the built-in Python3 venv module (not installed by default) 3 | # * Install gcc libpython3-dev to compile C Python modules 4 | # * In the virtualenv: Update pip setuputils and wheel to support building new packages 5 | FROM debian:12-slim AS build 6 | RUN apt-get update && \ 7 | apt-get install --no-install-suggests --no-install-recommends --yes python3-venv gcc libpython3-dev && \ 8 | python3 -m venv /venv && \ 9 | /venv/bin/pip install --upgrade pip setuptools wheel 10 | 11 | # Build the virtualenv as a separate step: Only re-execute this step when requirements.txt changes 12 | FROM build AS build-venv 13 | COPY requirements.txt /requirements.txt 14 | RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt 15 | 16 | # Copy the virtualenv into a distroless image 17 | FROM gcr.io/distroless/python3-debian12 18 | COPY --from=build-venv /venv /venv 19 | COPY . /app 20 | WORKDIR /app 21 | ENTRYPOINT ["/venv/bin/python3", "psutil_example.py"] 22 | -------------------------------------------------------------------------------- /examples/python3-requirements/README.md: -------------------------------------------------------------------------------- 1 | # Python 3 with requirements.txt 2 | 3 | This is a Python 3 application that specifies third-party dependencies using requirements.txt. The 4 | psutil module it uses is a C module that must be compiled. This is the most annoying kind of 5 | dependency, since you need to build it in an environment where the C library and Python version 6 | match where it will run. 7 | 8 | It builds the final container in three stages: 9 | 10 | 1. `build`: Set up a Debian build environment that can compile Python C modules. 11 | 2. `build-venv`: Create a virtualenv using `requirements.txt`. 12 | 3. Output: Copy the venv and the code and build the final image. 13 | 14 | The first step is only re-executed if you edit `Dockerfile`. The second step is only re-executed 15 | if you change requirements.txt. The final step is very fast and will change on every code edit. 16 | 17 | 18 | ## Build and run 19 | 20 | 1. Build the image: `docker build . --tag=psutil-example` 21 | 2. Run it! `docker run --rm psutil-example` 22 | 23 | 24 | ## Example output 25 | 26 | ``` 27 | RSS: 13.0 MiB; SHARED: 5.7 MiB; VIRTUAL: 19.9 MiB 28 | ``` 29 | 30 | 31 | ## TODO: Bazel 32 | 33 | It would be nice if we could show how to build this container with Bazel, but I don't actually know 34 | how to do that. 35 | -------------------------------------------------------------------------------- /examples/python3-requirements/psutil_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Distroless's test.sh runs pylint on all python files, but this module will not exist 4 | # pylint: disable=import-error 5 | import psutil 6 | 7 | 8 | def mib(total_bytes): 9 | '''Converts bytes to MiB (float).''' 10 | return total_bytes / 1024 / 1024 11 | 12 | 13 | def main(): 14 | current_process = psutil.Process() 15 | memory = current_process.memory_info() 16 | print(f'RSS: {mib(memory.rss):.1f} MiB; SHARED: {mib(memory.shared):.1f} MiB; VIRTUAL: {mib(memory.vms):.1f} MiB') 17 | 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /examples/python3-requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | # this version of psutil does not ship binary wheels: must be compiled 2 | psutil==5.6.6 3 | -------------------------------------------------------------------------------- /examples/python3/BUILD: -------------------------------------------------------------------------------- 1 | # Public notice: this file is for internal documentation, testing, and 2 | # reference only. Note that repo maintainers can freely change any part of the 3 | # repository code at any time. 4 | load("@rules_oci//oci:defs.bzl", "oci_image", "oci_tarball") 5 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 6 | load("//base:distro.bzl", "DISTROS") 7 | 8 | pkg_tar( 9 | name = "hello_py", 10 | srcs = ["hello.py"], 11 | ) 12 | 13 | # This example runs a python program that walks the filesystem under "/etc" and prints every filename. 14 | oci_image( 15 | name = "hello_debian12", 16 | base = "//python3:python3_root_amd64_debian12", 17 | cmd = [ 18 | "hello.py", 19 | "/etc", 20 | ], 21 | tars = [ 22 | ":hello_py", 23 | ], 24 | ) 25 | 26 | # Run 27 | # podman load -i bazel-bin/examples/python3/tarball/tarball.tar 28 | # podman run localhost/distroless/examples/py:latest 29 | [ 30 | oci_tarball( 31 | name = "tarball_" + distro, 32 | image = ":hello_" + distro, 33 | repo_tags = ["distroless/examples/py:latest"], 34 | ) 35 | for distro in DISTROS 36 | ] 37 | -------------------------------------------------------------------------------- /examples/python3/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim AS build-env 2 | COPY . /app 3 | WORKDIR /app 4 | 5 | FROM gcr.io/distroless/python3 6 | COPY --from=build-env /app /app 7 | WORKDIR /app 8 | CMD ["hello.py", "/etc"] 9 | -------------------------------------------------------------------------------- /examples/python3/hello.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | # Copyright 2017 Google Inc. All rights reserved. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import argparse 18 | import os 19 | 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument('root', type=str, 22 | help='The root directory to walk.') 23 | 24 | def main(args): 25 | """Prints the files that are inside the container, rooted at the first argument.""" 26 | for dirpath, _, files in os.walk(args.root): 27 | for f in files: 28 | print(os.path.join(dirpath, f)) 29 | 30 | if __name__ == "__main__": 31 | main(parser.parse_args()) 32 | -------------------------------------------------------------------------------- /examples/rust/.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | target/ 3 | -------------------------------------------------------------------------------- /examples/rust/BUILD: -------------------------------------------------------------------------------- 1 | # Public notice: this file is for internal documentation, testing, and 2 | # reference only. Note that repo maintainers can freely change any part of the 3 | # repository code at any time. 4 | 5 | # Rust 6 | load("//private/oci:defs.bzl", "rust_image") 7 | 8 | package(default_visibility = ["//visibility:public"]) 9 | 10 | # NOTE: Bazel Rust rules don't support cross-compilation yet, 11 | # so at this time it's required that you build on the same platform as the image 12 | rust_image( 13 | name = "rust_example", 14 | srcs = ["src/main.rs"], 15 | base = "//cc:cc_root_amd64_debian12", 16 | tags = [ 17 | "amd64", 18 | "manual", 19 | ], 20 | ) 21 | -------------------------------------------------------------------------------- /examples/rust/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "hello-world-distroless" 5 | version = "0.1.0" 6 | -------------------------------------------------------------------------------- /examples/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world-distroless" 3 | version = "0.1.0" 4 | authors = ["Bryant Hagadorn "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /examples/rust/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1 as build-env 2 | WORKDIR /app 3 | COPY . /app 4 | RUN cargo build --release 5 | 6 | FROM gcr.io/distroless/cc-debian12 7 | COPY --from=build-env /app/target/release/hello-world-distroless / 8 | CMD ["./hello-world-distroless"] 9 | -------------------------------------------------------------------------------- /examples/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello World!"); 3 | } 4 | -------------------------------------------------------------------------------- /examples/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | for d in examples/*; do 6 | # Skip non-directories. 7 | if [[ ! -d "$d" ]]; then continue; fi 8 | 9 | # Skip if the directory doesn't have a Dockerfile. 10 | if [[ ! -f $d/Dockerfile ]]; then continue; fi 11 | 12 | # Skip these non-working examples. 13 | if [[ $d == "examples/cc" ]]; then continue; fi 14 | 15 | # Build the Dockerfile and run the image, or fail. 16 | echo ==================================== 17 | echo = building $d 18 | echo ==================================== 19 | docker build -t $d ./$d/ 20 | docker run $d 21 | done 22 | -------------------------------------------------------------------------------- /experimental/BUILD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/distroless/4c40ba78166a7e559bcd41df2083d45bdb4212e7/experimental/BUILD -------------------------------------------------------------------------------- /experimental/busybox/BUILD: -------------------------------------------------------------------------------- 1 | load("//:checksums.bzl", "ARCHITECTURES") 2 | load(":commands.bzl", "busybox_layer") 3 | 4 | package(default_visibility = ["//base:__subpackages__"]) 5 | 6 | # This works for all architectures because it is just files. 7 | # Ppc64le/Arm64/Amd64 needs special case as there is no direct working binary file available. 8 | SPECIAL_CASE_ARCH = [ 9 | "ppc64le", 10 | "arm64", 11 | "amd64", 12 | ] 13 | 14 | [ 15 | busybox_layer( 16 | name = "busybox_" + arch, 17 | busybox = "@busybox_" + arch + "//:file" if arch in SPECIAL_CASE_ARCH else "@busybox_" + arch + "//file", 18 | compress = "gzip", 19 | ) 20 | for arch in ARCHITECTURES 21 | ] 22 | -------------------------------------------------------------------------------- /experimental/busybox/commands.bzl: -------------------------------------------------------------------------------- 1 | load("@aspect_bazel_lib//lib:tar.bzl", "tar") 2 | 3 | BUSYBOX_COMMANDS = [ 4 | "[", 5 | "[[", 6 | "acpid", 7 | "add-shell", 8 | "addgroup", 9 | "adduser", 10 | "adjtimex", 11 | "ar", 12 | "arp", 13 | "arping", 14 | "ash", 15 | "awk", 16 | "base64", 17 | "basename", 18 | "blkdiscard", 19 | "blkid", 20 | "blockdev", 21 | "bootchartd", 22 | "brctl", 23 | "bunzip2", 24 | "bzcat", 25 | "bzip2", 26 | "cal", 27 | "cat", 28 | "chat", 29 | "chattr", 30 | "chgrp", 31 | "chmod", 32 | "chown", 33 | "chpasswd", 34 | "chpst", 35 | "chroot", 36 | "chrt", 37 | "chvt", 38 | "cksum", 39 | "clear", 40 | "cmp", 41 | "comm", 42 | "conspy", 43 | "cp", 44 | "cpio", 45 | "crond", 46 | "crontab", 47 | "cryptpw", 48 | "cttyhack", 49 | "cut", 50 | "date", 51 | "dc", 52 | "dd", 53 | "deallocvt", 54 | "delgroup", 55 | "deluser", 56 | "depmod", 57 | "devmem", 58 | "df", 59 | "dhcprelay", 60 | "diff", 61 | "dirname", 62 | "dmesg", 63 | "dnsd", 64 | "dnsdomainname", 65 | "dos2unix", 66 | "dpkg", 67 | "dpkg-deb", 68 | "du", 69 | "dumpkmap", 70 | "dumpleases", 71 | "echo", 72 | "ed", 73 | "egrep", 74 | "eject", 75 | "env", 76 | "envdir", 77 | "envuidgid", 78 | "expand", 79 | "expr", 80 | "factor", 81 | "fakeidentd", 82 | "false", 83 | "fatattr", 84 | "fbset", 85 | "fbsplash", 86 | "fdflush", 87 | "fdformat", 88 | "fdisk", 89 | "fgconsole", 90 | "fgrep", 91 | "find", 92 | "findfs", 93 | "flash_eraseall", 94 | "flash_lock", 95 | "flash_unlock", 96 | "flashcp", 97 | "flock", 98 | "fold", 99 | "free", 100 | "freeramdisk", 101 | "fsck", 102 | "fsck.minix", 103 | "fsfreeze", 104 | "fstrim", 105 | "fsync", 106 | "ftpd", 107 | "ftpget", 108 | "ftpput", 109 | "fuser", 110 | "getopt", 111 | "getty", 112 | "grep", 113 | "groups", 114 | "gunzip", 115 | "gzip", 116 | "halt", 117 | "hd", 118 | "hdparm", 119 | "head", 120 | "hexdump", 121 | "hostid", 122 | "hostname", 123 | "httpd", 124 | "hush", 125 | "hwclock", 126 | "i2cdetect", 127 | "i2cdump", 128 | "i2cget", 129 | "i2cset", 130 | "id", 131 | "ifconfig", 132 | "ifenslave", 133 | "ifplugd", 134 | "inetd", 135 | "init", 136 | "inotifyd", 137 | "insmod", 138 | "install", 139 | "ionice", 140 | "iostat", 141 | "ip", 142 | "ipaddr", 143 | "ipcalc", 144 | "ipcrm", 145 | "ipcs", 146 | "iplink", 147 | "ipneigh", 148 | "iproute", 149 | "iprule", 150 | "iptunnel", 151 | "kbd_mode", 152 | "kill", 153 | "killall", 154 | "killall5", 155 | "klogd", 156 | "last", 157 | "less", 158 | "link", 159 | "linux32", 160 | "linux64", 161 | "linuxrc", 162 | "ln", 163 | "loadfont", 164 | "loadkmap", 165 | "logger", 166 | "login", 167 | "logname", 168 | "losetup", 169 | "lpd", 170 | "lpq", 171 | "lpr", 172 | "ls", 173 | "lsattr", 174 | "lsmod", 175 | "lsof", 176 | "lspci", 177 | "lsscsi", 178 | "lsusb", 179 | "lzcat", 180 | "lzma", 181 | "lzop", 182 | "lzopcat", 183 | "makedevs", 184 | "makemime", 185 | "man", 186 | "md5sum", 187 | "mdev", 188 | "mesg", 189 | "microcom", 190 | "mkdir", 191 | "mkdosfs", 192 | "mke2fs", 193 | "mkfifo", 194 | "mkfs.ext2", 195 | "mkfs.minix", 196 | "mkfs.vfat", 197 | "mknod", 198 | "mkpasswd", 199 | "mkswap", 200 | "mktemp", 201 | "modinfo", 202 | "modprobe", 203 | "more", 204 | "mount", 205 | "mountpoint", 206 | "mpstat", 207 | "mt", 208 | "mv", 209 | "nameif", 210 | "nbd-client", 211 | "nc", 212 | "netstat", 213 | "nice", 214 | "nl", 215 | "nmeter", 216 | "nohup", 217 | "nproc", 218 | "ntpd", 219 | "od", 220 | "openvt", 221 | "partprobe", 222 | "passwd", 223 | "paste", 224 | "patch", 225 | "pgrep", 226 | "pidof", 227 | "ping", 228 | "ping6", 229 | "pipe_progress", 230 | "pivot_root", 231 | "pkill", 232 | "pmap", 233 | "popmaildir", 234 | "poweroff", 235 | "powertop", 236 | "printenv", 237 | "printf", 238 | "ps", 239 | "pscan", 240 | "pstree", 241 | "pwd", 242 | "pwdx", 243 | "raidautorun", 244 | "rdate", 245 | "rdev", 246 | "readlink", 247 | "readprofile", 248 | "realpath", 249 | "reboot", 250 | "reformime", 251 | "remove-shell", 252 | "renice", 253 | "reset", 254 | "resize", 255 | "rev", 256 | "rm", 257 | "rmdir", 258 | "rmmod", 259 | "route", 260 | "rpm", 261 | "rpm2cpio", 262 | "rtcwake", 263 | "run-parts", 264 | "runlevel", 265 | "runsv", 266 | "runsvdir", 267 | "rx", 268 | "script", 269 | "scriptreplay", 270 | "sed", 271 | "sendmail", 272 | "seq", 273 | "setarch", 274 | "setconsole", 275 | "setfont", 276 | "setkeycodes", 277 | "setlogcons", 278 | "setpriv", 279 | "setserial", 280 | "setsid", 281 | "setuidgid", 282 | "sh", 283 | "sha1sum", 284 | "sha256sum", 285 | "sha3sum", 286 | "sha512sum", 287 | "showkey", 288 | "shred", 289 | "shuf", 290 | "slattach", 291 | "sleep", 292 | "smemcap", 293 | "softlimit", 294 | "sort", 295 | "split", 296 | "ssl_client", 297 | "start-stop-daemon", 298 | "stat", 299 | "strings", 300 | "stty", 301 | "su", 302 | "sulogin", 303 | "sum", 304 | "sv", 305 | "svc", 306 | "svlogd", 307 | "swapoff", 308 | "swapon", 309 | "switch_root", 310 | "sync", 311 | "sysctl", 312 | "syslogd", 313 | "tac", 314 | "tail", 315 | "tar", 316 | "taskset", 317 | "tcpsvd", 318 | "tee", 319 | "telnet", 320 | "telnetd", 321 | "test", 322 | "tftp", 323 | "tftpd", 324 | "time", 325 | "timeout", 326 | "top", 327 | "touch", 328 | "tr", 329 | "traceroute", 330 | "traceroute6", 331 | "true", 332 | "truncate", 333 | "tty", 334 | "ttysize", 335 | "tunctl", 336 | "tune2fs", 337 | "ubiattach", 338 | "ubidetach", 339 | "ubimkvol", 340 | "ubirename", 341 | "ubirmvol", 342 | "ubirsvol", 343 | "ubiupdatevol", 344 | "udhcpc", 345 | "udhcpd", 346 | "udpsvd", 347 | "uevent", 348 | "umount", 349 | "uname", 350 | "uncompress", 351 | "unexpand", 352 | "uniq", 353 | "unix2dos", 354 | "unlink", 355 | "unlzma", 356 | "unlzop", 357 | "unxz", 358 | "unzip", 359 | "uptime", 360 | "users", 361 | "usleep", 362 | "uudecode", 363 | "uuencode", 364 | "vconfig", 365 | "vi", 366 | "vlock", 367 | "volname", 368 | "w", 369 | "wall", 370 | "watch", 371 | "watchdog", 372 | "wc", 373 | "wget", 374 | "which", 375 | "who", 376 | "whoami", 377 | "whois", 378 | "xargs", 379 | "xxd", 380 | "xz", 381 | "xzcat", 382 | "yes", 383 | "zcat", 384 | "zcip", 385 | ] 386 | 387 | BUSYBOX_ARCHIVE_BUILD = """\ 388 | filegroup( 389 | name = "file", 390 | srcs = ["bin/busybox"], 391 | visibility = ["//visibility:public"] 392 | ) 393 | """ 394 | 395 | def busybox_layer(busybox, **kwargs): 396 | tar( 397 | srcs = [busybox], 398 | mtree = [ 399 | "./busybox/ uid=0 gid=0 mode=0755 time=0.0 type=dir", 400 | "./busybox/busybox uid=0 gid=0 mode=0755 time=0.0 type=file content=$(location {})".format(busybox), 401 | ] + [ 402 | "./busybox/{cmd} uid=0 gid=0 mode=0755 time=0.0 type=link link=/busybox/busybox".format(cmd = cmd) 403 | for cmd in BUSYBOX_COMMANDS 404 | ], 405 | **kwargs 406 | ) 407 | -------------------------------------------------------------------------------- /java/README.md: -------------------------------------------------------------------------------- 1 | # Documentation for `gcr.io/distroless/java` 2 | 3 | ## Image Contents 4 | 5 | This image contains a minimal Linux, OpenJDK-based runtime. 6 | 7 | Specifically, the image contains everything in the [base image](../base/README.md), plus: 8 | 9 | * OpenJDK 17 (`gcr.io/distroless/java17-debian12`) and its dependencies. 10 | * Temurin OpenJDK 21 (`gcr.io/distroless/java21-debian12`) and its dependencies 11 | 12 | 13 | ## Usage 14 | 15 | The entrypoint of this image is set to the equivalent of "java -jar", so this image expects users to supply a path to a JAR file in the CMD. 16 | -------------------------------------------------------------------------------- /java/control: -------------------------------------------------------------------------------- 1 | Package: Eclipse Temurin 2 | Version: {{VERSION}} 3 | Architecture: {{ARCHITECTURE}} 4 | Maintainer: Adoptium Working Group 5 | Homepage: https://adoptium.net 6 | SHA256: {{SHA256}} 7 | Description: Eclipse Temurin is the name of the OpenJDK distribution from Adoptium. 8 | -------------------------------------------------------------------------------- /java/jre_ver.bzl: -------------------------------------------------------------------------------- 1 | def jre_ver(version): 2 | """Extract JRE version from a Debian openjdk downloaded filename. 3 | 4 | Debian packages versions are of the form: 5 | openjdk-11-jre*: 11.0.1+13-2~bpo9+1 6 | """ 7 | if version.startswith("11.") or version.startswith("17."): 8 | return version.split("+")[0] 9 | 10 | fail("unrecognized openjdk package version: " + version) 11 | -------------------------------------------------------------------------------- /java/testdata/CheckCerts.java: -------------------------------------------------------------------------------- 1 | package testdata; 2 | 3 | import java.io.IOException; 4 | import java.net.URL; 5 | import javax.net.ssl.HttpsURLConnection; 6 | 7 | public class CheckCerts { 8 | 9 | public static void main(String[] args) throws IOException { 10 | URL url = new URL("https://www.google.com/"); 11 | HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); 12 | conn.connect(); 13 | conn.getServerCertificates(); // succeeds if peer verified 14 | 15 | System.out.println("Successfully connected: " + conn.getResponseCode()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /java/testdata/CheckEncoding.java: -------------------------------------------------------------------------------- 1 | package testdata; 2 | 3 | import java.nio.charset.Charset; 4 | import java.util.Locale; 5 | 6 | public class CheckEncoding { 7 | 8 | public static void main(String[] args) { 9 | System.out.println("LANG=" + System.getenv("LANG")); 10 | System.out.println("Locale.getDefault()=" + Locale.getDefault()); 11 | System.out.println("Charset.defaultCharset()=" + Charset.defaultCharset()); 12 | System.out.println("file.encoding=" + System.getProperty("file.encoding")); 13 | System.out.println("sun.jnu.encoding=" + System.getProperty("sun.jnu.encoding")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /java/testdata/CheckLibharfbuzz.java: -------------------------------------------------------------------------------- 1 | package testdata; 2 | 3 | import java.awt.GraphicsEnvironment; 4 | import java.awt.Font; 5 | 6 | public class CheckLibharfbuzz { 7 | public static void main(String[] args) { 8 | GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); 9 | Font [] fonts = env.getAllFonts(); 10 | if ( fonts.length > 0 ) { 11 | System.out.println(fonts.length + " fonts available"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /java/testdata/java17_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: java 4 | command: "/usr/lib/jvm/java-17-openjdk-amd64/bin/java" 5 | args: ["-version"] 6 | expectedError: ['openjdk version "17.0.15"'] 7 | - name: java-symlink 8 | command: "/usr/bin/java" 9 | args: ["-version"] 10 | expectedError: ['openjdk version "17.0.15"'] 11 | fileExistenceTests: 12 | - name: certs 13 | path: "/etc/ssl/certs/java/cacerts" 14 | shouldExist: true 15 | - name: no-busybox 16 | path: "/busybox/sh" 17 | shouldExist: false 18 | - name: no-shell 19 | path: "/bin/sh" 20 | shouldExist: false 21 | - name: no-javac 22 | path: "/usr/lib/jvm/java-17-openjdk-amd64/bin/javac" 23 | shouldExist: false 24 | metadataTest: 25 | envVars: 26 | - key: 'JAVA_VERSION' 27 | value: '17.0.15' 28 | -------------------------------------------------------------------------------- /java/testdata/java17_debug_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: java 4 | command: "/usr/lib/jvm/java-17-openjdk-amd64/bin/java" 5 | args: ["-version"] 6 | expectedError: ['openjdk version "17.0.15"'] 7 | - name: java-symlink 8 | command: "/usr/bin/java" 9 | args: ["-version"] 10 | expectedError: ['openjdk version "17.0.15"'] 11 | - name: javac 12 | command: "/usr/lib/jvm/java-17-openjdk-amd64/bin/javac" 13 | args: ["-version"] 14 | expectedOutput: ['javac 17.0.15'] 15 | fileExistenceTests: 16 | - name: certs 17 | path: "/etc/ssl/certs/java/cacerts" 18 | shouldExist: true 19 | - name: busybox 20 | path: "/busybox/sh" 21 | shouldExist: true 22 | - name: no-shell 23 | path: "/bin/sh" 24 | shouldExist: false 25 | metadataTest: 26 | envVars: 27 | - key: 'JAVA_VERSION' 28 | value: '17.0.15' 29 | -------------------------------------------------------------------------------- /java/testdata/java21_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: java 4 | command: "/usr/lib/jvm/temurin21_jre_amd64/bin/java" 5 | args: ["-version"] 6 | expectedError: ['openjdk version "21.0.7"'] 7 | - name: java-symlink 8 | command: "/usr/bin/java" 9 | args: ["-version"] 10 | expectedError: ['openjdk version "21.0.7"'] 11 | fileExistenceTests: 12 | - name: certs 13 | path: "/etc/ssl/certs/java/cacerts" 14 | shouldExist: true 15 | - name: certs 16 | path: "/usr/lib/jvm/temurin21_jre_amd64/lib/security/cacerts" 17 | permissions: 'Lrwxrwxrwx' 18 | shouldExist: true 19 | - name: no-busybox 20 | path: "/busybox/sh" 21 | shouldExist: false 22 | - name: no-shell 23 | path: "/bin/sh" 24 | shouldExist: false 25 | - name: no-javac 26 | path: "/usr/lib/jvm/temurin21_jre_amd64/bin/javac" 27 | shouldExist: false 28 | - name: jexec-executable 29 | path: "/usr/lib/jvm/temurin21_jre_amd64/lib/jexec" 30 | shouldExist: true 31 | isExecutableBy: "any" 32 | - name: jspawnhelper-executable 33 | path: "/usr/lib/jvm/temurin21_jre_amd64/lib/jspawnhelper" 34 | shouldExist: true 35 | isExecutableBy: "any" 36 | metadataTest: 37 | envVars: 38 | - key: 'JAVA_VERSION' 39 | value: '21.0.7' 40 | -------------------------------------------------------------------------------- /java/testdata/java21_debug_debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: java 4 | command: "/usr/lib/jvm/temurin21_jdk_amd64/bin/java" 5 | args: ["-version"] 6 | expectedError: ['openjdk version "21.0.7"'] 7 | - name: java-symlink 8 | command: "/usr/bin/java" 9 | args: ["-version"] 10 | expectedError: ['openjdk version "21.0.7"'] 11 | - name: javac 12 | command: "/usr/lib/jvm/temurin21_jdk_amd64/bin/javac" 13 | args: ["-version"] 14 | expectedOutput: ['javac 21.0.7'] 15 | fileExistenceTests: 16 | - name: certs 17 | path: "/etc/ssl/certs/java/cacerts" 18 | shouldExist: true 19 | - name: certs 20 | path: "/usr/lib/jvm/temurin21_jdk_amd64/lib/security/cacerts" 21 | permissions: 'Lrwxrwxrwx' 22 | shouldExist: true 23 | - name: busybox 24 | path: "/busybox/sh" 25 | shouldExist: true 26 | - name: no-shell 27 | path: "/bin/sh" 28 | shouldExist: false 29 | metadataTest: 30 | envVars: 31 | - key: 'JAVA_VERSION' 32 | value: '21.0.7' 33 | -------------------------------------------------------------------------------- /java/testdata/java_base.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | fileExistenceTests: 3 | - name: certs 4 | path: "/etc/ssl/certs/java/cacerts" 5 | shouldExist: true 6 | - name: no-busybox 7 | path: "/busybox/sh" 8 | shouldExist: false 9 | - name: no-shell 10 | path: "/bin/sh" 11 | shouldExist: false 12 | - name: no-jvm 13 | path: "/usr/lib/jvm" 14 | shouldExist: false 15 | -------------------------------------------------------------------------------- /java/testdata/java_base_debug.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | fileExistenceTests: 3 | - name: certs 4 | path: "/etc/ssl/certs/java/cacerts" 5 | shouldExist: true 6 | - name: busybox 7 | path: "/busybox/sh" 8 | shouldExist: true 9 | - name: no-shell 10 | path: "/bin/sh" 11 | shouldExist: false 12 | - name: no-jvm 13 | path: "/usr/lib/jvm" 14 | shouldExist: false 15 | -------------------------------------------------------------------------------- /java/testdata/java_certs.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: connect_to_https_google_com 4 | # This is a bit ugly because structure tests can't test the default entrypoint yet. 5 | command: ["/usr/bin/java", 6 | "-cp", 7 | "/*", 8 | "testdata.CheckCerts"] 9 | expectedOutput: ['Successfully connected: 200'] 10 | -------------------------------------------------------------------------------- /java/testdata/java_encoding.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: check_encoding 4 | command: ["/usr/bin/java", 5 | "-cp", 6 | "/*", 7 | "testdata.CheckEncoding"] 8 | expectedOutput: ['LANG=C.UTF-8', 9 | 'Locale.getDefault\(\)=en', 10 | 'Charset.defaultCharset\(\)=UTF-8', 11 | 'file.encoding=UTF-8', 12 | 'sun.jnu.encoding=UTF-8'] 13 | -------------------------------------------------------------------------------- /java/testdata/java_libharfbuzz.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: check_libharfbuzz 4 | command: ["/usr/bin/java", 5 | "-cp", 6 | "/*", 7 | "testdata.CheckLibharfbuzz"] 8 | expectedOutput: ['^\d+ fonts available'] 9 | -------------------------------------------------------------------------------- /knife: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | # Copyright 2024 Google Inc. All rights reserved. 5 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | if [ $(uname) == "Darwin" ]; then 19 | echo "WARNING: You are on a macos, you need to run 'brew install coreutils gnu-sed' to install required packages." 20 | echo "" 21 | export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH" 22 | export PATH="/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH" 23 | fi 24 | 25 | function cmd_lock() { 26 | echo "🚧 Querying for repos (temporarily using hardcoded repos)" 27 | echo "" 28 | # temporarily hardcode right now (query doesn't work after bzl mod) 29 | local repos=$(cat </dev/null | cut -d: -f2) 36 | 37 | for repo in $repos; do 38 | for i in $(seq 10); do 39 | echo "🔑 Locking $repo (attempt ${i})" 40 | bazel run "@${repo}//:lock" && break || sleep 20; 41 | if [[ $i -eq 10 ]]; then 42 | echo "" 43 | echo "Failed to lock $repo after 10 attempts" >&2 44 | exit 1 45 | fi 46 | done 47 | done 48 | } 49 | 50 | function find_latest_snapshot() { 51 | local type="$1" 52 | # If it's the first of the month, look at the last month, otherwise this -1 day has no effect since searches 53 | # occur at the "month" level. This is an intentional buffer added to get the snapshots fully hydrated. We 54 | # intentionally don't include complicated logic for the case where it's after the 1st and no snapshots are 55 | # availalbe for the month (it's extremely unlikely for our updater to run into this situation unless the 56 | # snapshot serving infrastructure is acting up). 57 | local current="$(date -d '-1 day' +%Y-%m-%d)" 58 | local tmp=$(mktemp) 59 | local q=$(date -d "$current" +"year=%Y&month=%m") 60 | if curl -fs "https://snapshot.debian.org/archive/debian/?$q" | grep -ohE "([0-9]+T[0-9]+Z)" > $tmp; then 61 | # same logic as above, find the newest snapshot that isn't "today" 62 | today=$(date +"%Y%m%dT") 63 | cat $tmp | grep -v $today | tail -n1 64 | fi 65 | } 66 | 67 | function cmd_update_snapshots() { 68 | echo "🧐 Looking for updates... " 69 | latest=$(find_latest_snapshot "debian") 70 | latest_security=$(find_latest_snapshot "debian-security") 71 | if [[ -z "$latest" || -z "$latest_security" ]]; then 72 | echo "" 73 | echo "could not find any snapshots for debian or debian-security" 74 | exit 1 75 | fi 76 | echo "" 77 | echo "🎯 Found snapshots" 78 | echo " debian: $latest" 79 | echo " security: $latest_security" 80 | echo "" 81 | 82 | # if tty ask for approval 83 | if [ -t 1 ]; then 84 | read -p "Do you want to continue? (y/n) " -n 1 -r 85 | sleep 0.5 86 | echo $'\n' 87 | if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then 88 | echo "Aborting..." 89 | exit 0 90 | fi 91 | fi 92 | 93 | for mpath in "./private/repos/deb/"*.yaml; do 94 | current=$(grep -oE "debian/([0-9]+T[0-9]+Z)" $mpath | cut -d/ -f2 | head -n1) 95 | current_security=$(grep -oE "debian-security/([0-9]+T[0-9]+Z)" $mpath | cut -d/ -f2 | head -n1) 96 | 97 | if [[ "$current" == "$latest" && "$current_security" == "$latest_security" ]]; then 98 | echo "🎖️ $mpath is up to date." 99 | continue 100 | fi 101 | echo "🗞️ $mpath" 102 | if [[ "$current" != "$latest" ]]; then 103 | sed -i -E "s/(debian\/)([0-9]+T[0-9]+Z)/\1$latest/" "$mpath" 104 | echo " debian: $current -> $latest" 105 | fi 106 | if [[ "$current_security" != "$latest_security" ]]; then 107 | sed -i -E "s/(debian-security\/)([0-9]+T[0-9]+Z)/\1$latest_security/" "$mpath" 108 | echo " debian-security: $current_security -> $latest_security" 109 | fi 110 | echo "" 111 | done 112 | echo "" 113 | echo "👌 Done..." 114 | } 115 | 116 | # write DISTROLESS_DIFF to GITHUB_ENV if changes are made 117 | function cmd_github_update_snapshots() { 118 | local tmp=$(mktemp -d) 119 | jq -nr 'inputs.packages[] | .key + " " + .sha256' ./private/repos/deb/*.lock.json | sort > "$tmp/old.hashes" 120 | cmd_update_snapshots 121 | cmd_lock 122 | jq -nr 'inputs.packages[] | .key + " " + .sha256' ./private/repos/deb/*.lock.json | sort > "$tmp/new.hashes" 123 | diff "$tmp/old.hashes" "$tmp/new.hashes" | tee "$tmp/diff" || printf "DISTROLESS_DIFF<> "$GITHUB_ENV" 124 | } 125 | 126 | function cmd_lint () { 127 | echo "🧹 Linting" 128 | echo "" 129 | if ! which buildifier > /dev/null; then 130 | echo "🧱 No buildifier executable was found." 131 | echo " Did you follow the ./CONTRIBUTING.md ?" 132 | exit 1 133 | fi 134 | buildifier -mode=fix $(find . -name 'BUILD*' -o -name 'WORKSPACE*' -o -name '*.bzl' -type f) 135 | } 136 | 137 | function cmd_update_java_archives () { 138 | source scripts/update_java_archives.sh 139 | old_version=$(get_java_version) 140 | generate_java_archives > private/repos/java.MODULE.bazel 141 | new_version=$(get_java_version) 142 | update_test_versions_java21 $old_version $new_version 143 | } 144 | 145 | function cmd_update_node_archives () { 146 | if ! which jq > /dev/null; then 147 | echo "🧱 No jq executable was found" 148 | exit 1 149 | fi 150 | if ! which curl > /dev/null; then 151 | echo "🧱 No curl executable was found" 152 | exit 1 153 | fi 154 | if ! which node > /dev/null; then 155 | echo "🧱 No node executable was found" 156 | exit 1 157 | fi 158 | 159 | versions=() 160 | for major in 20 22 24; do 161 | latest_version=$(curl -sSL https://nodejs.org/dist/index.json | jq -r --arg major "v$major" ' 162 | map(select(.version | startswith($major))) 163 | | sort_by(.date) | reverse | .[0].version 164 | ') 165 | latest_nov=$(echo "$latest_version" | sed 's/^v//') 166 | versions+=("$latest_nov") 167 | done 168 | 169 | joined_versions=$(IFS=, ; echo "${versions[*]}") 170 | node scripts/update_node_archives.js "$joined_versions" 171 | } 172 | 173 | 174 | function cmd_test () { 175 | echo "🧪 Testing" 176 | echo "" 177 | 178 | local arch=$(uname -m) 179 | 180 | echo "💡 only including image tests for $arch" 181 | echo "" 182 | 183 | arch_specific_targets=$(bazel query "attr(\"tags\", "$arch", \"//...\")") 184 | 185 | # Run all tests tagged with "amd64" 186 | bazel test --test_timeout=900 //... $arch_specific_targets 187 | } 188 | 189 | function cmd_deb_versions () { 190 | echo "🔧 Printing .deb Versions (bookworm) from private/repos/deb/bookworm*.lock.json" 191 | echo "" 192 | 193 | jq -n '[inputs.packages[]] | group_by(.arch) | map({(.[0].arch): map({package: .name, version: .version})})' private/repos/deb/bookworm*.lock.json 194 | } 195 | 196 | case "${1:-"~~nocmd"}" in 197 | lock) 198 | cmd_lock 199 | ;; 200 | update-snapshots) 201 | cmd_update_snapshots 202 | ;; 203 | lint) 204 | cmd_lint 205 | ;; 206 | github-update-snapshots) 207 | cmd_github_update_snapshots 208 | ;; 209 | test) 210 | cmd_test 211 | ;; 212 | update-java-archives) 213 | cmd_update_java_archives 214 | ;; 215 | deb-versions) 216 | cmd_deb_versions 217 | ;; 218 | update-node-archives) 219 | cmd_update_node_archives 220 | ;; 221 | ~~nocmd) # no command provided 222 | echo "provide a command: lock, update-snapshots, github-update-snapshots, update-java-archives, test, deb-versions, update-node-archives" 223 | exit 1 224 | ;; 225 | *) # unknown command 226 | echo "unknown command $1" 227 | exit 1 228 | ;; 229 | esac 230 | -------------------------------------------------------------------------------- /nodejs/BUILD: -------------------------------------------------------------------------------- 1 | load("@container_structure_test//:defs.bzl", "container_structure_test") 2 | load("@rules_oci//oci:defs.bzl", "oci_image", "oci_image_index") 3 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 4 | load("//base:distro.bzl", "DISTROS") 5 | load("//nodejs:node_arch.bzl", "node_arch") 6 | 7 | package(default_visibility = ["//visibility:public"]) 8 | 9 | NODEJS_MAJOR_VERISONS = ("20", "22", "24") 10 | 11 | MODE = [ 12 | "", 13 | "_debug", 14 | ] 15 | 16 | USER = [ 17 | "root", 18 | "nonroot", 19 | ] 20 | 21 | [ 22 | oci_image_index( 23 | name = "nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + distro, 24 | images = [ 25 | "nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro 26 | for arch in node_arch(major_version) 27 | ], 28 | ) 29 | for mode in MODE 30 | for user in USER 31 | for major_version in NODEJS_MAJOR_VERISONS 32 | for distro in DISTROS 33 | ] 34 | 35 | [ 36 | oci_image( 37 | name = "nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro, 38 | base = ("//cc:cc" if (not ("debug" in mode)) else "//cc:debug") + "_" + user + "_" + arch + "_" + distro, 39 | entrypoint = ["/nodejs/bin/node"], 40 | tars = [ 41 | "@nodejs" + major_version + "_" + arch, 42 | ], 43 | ) 44 | for mode in MODE 45 | for user in USER 46 | for major_version in NODEJS_MAJOR_VERISONS 47 | for arch in node_arch(major_version) 48 | for distro in DISTROS 49 | ] 50 | 51 | [ 52 | container_structure_test( 53 | name = "nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro + "_test", 54 | configs = [ 55 | "testdata/nodejs" + major_version + ".yaml", 56 | "testdata/check_headers.yaml", 57 | "testdata/check_npm.yaml", 58 | ], 59 | image = "nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro, 60 | tags = [ 61 | arch, 62 | "manual", 63 | ], 64 | ) 65 | for mode in MODE 66 | for user in USER 67 | for major_version in NODEJS_MAJOR_VERISONS 68 | for arch in node_arch(major_version) 69 | for distro in DISTROS 70 | ] 71 | 72 | pkg_tar( 73 | name = "check_certificate", 74 | srcs = ["testdata/check_certificate.js"], 75 | ) 76 | 77 | [ 78 | oci_image( 79 | name = "check_certificate_nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro, 80 | base = "nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro, 81 | tars = [ 82 | ":check_certificate", 83 | ], 84 | ) 85 | for mode in MODE 86 | for user in USER 87 | for major_version in NODEJS_MAJOR_VERISONS 88 | for arch in node_arch(major_version) 89 | for distro in DISTROS 90 | ] 91 | 92 | [ 93 | container_structure_test( 94 | name = "check_certificate_nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro + "_test", 95 | configs = ["testdata/check_certificate.yaml"], 96 | image = "check_certificate_nodejs" + major_version + ("" if (not mode) else mode) + "_" + user + "_" + arch + "_" + distro, 97 | tags = [ 98 | arch, 99 | "manual", 100 | ], 101 | ) 102 | for mode in MODE 103 | for user in USER 104 | for major_version in NODEJS_MAJOR_VERISONS 105 | for arch in node_arch(major_version) 106 | for distro in DISTROS 107 | ] 108 | -------------------------------------------------------------------------------- /nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation for `gcr.io/distroless/nodejs` 2 | 3 | ## Image Contents 4 | 5 | These images contain a minimal Linux, Node.js-based runtime. The supported versions match the [Node.js LTS releases](https://nodejs.org/en/about/previous-releases). 6 | 7 | Specifically, these images contain everything in the [base image](../base/README.md), plus one of: 8 | 9 | - Node.js v20 (`gcr.io/distroless/nodejs20-debian12`) and its dependencies. 10 | - Node.js v22 (`gcr.io/distroless/nodejs22-debian12`) and its dependencies. 11 | - Node.js v24 (`gcr.io/distroless/nodejs24-debian12`) and its dependencies. 12 | 13 | ## Usage 14 | 15 | The entrypoint of this image is set to "node", so this image expects users to supply a path to a .js file in the CMD. 16 | 17 | See the Node.js [Hello World](../examples/nodejs/) directory for an example. 18 | -------------------------------------------------------------------------------- /nodejs/control: -------------------------------------------------------------------------------- 1 | Package: nodejs 2 | Version: {{VERSION}} 3 | Architecture: {{ARCHITECTURE}} 4 | Maintainer: OpenJS Foundation 5 | Homepage: https://nodejs.org 6 | SHA256: {{SHA256}} 7 | Description: Node.js event-based server-side javascript engine 8 | Node.js is similar in design to and influenced by systems like 9 | Ruby's Event Machine or Python's Twisted. 10 | . 11 | It takes the event model a bit further - it presents the event 12 | loop as a language construct instead of as a library. 13 | . 14 | Node.js is bundled with several useful libraries to handle server tasks : 15 | System, Events, Standard I/O, Modules, Timers, Child Processes, POSIX, 16 | HTTP, Multipart Parsing, TCP, DNS, Assert, Path, URL, Query Strings. 17 | -------------------------------------------------------------------------------- /nodejs/node_arch.bzl: -------------------------------------------------------------------------------- 1 | load("//:checksums.bzl", "ARCHITECTURES") 2 | 3 | # Function to filter architectures based on Node.js version 4 | def node_arch(major_version): 5 | if int(major_version) >= 24: 6 | return [arch for arch in ARCHITECTURES if arch != "arm"] 7 | return ARCHITECTURES 8 | -------------------------------------------------------------------------------- /nodejs/testdata/check_certificate.js: -------------------------------------------------------------------------------- 1 | const https = require('https') 2 | const options = { 3 | hostname: 'www.google.com', 4 | port: 443, 5 | path: '/', 6 | method: 'GET' 7 | } 8 | 9 | const req = https.request(options, res => { 10 | console.log(`statusCode: ${res.statusCode}`) 11 | }) 12 | 13 | req.end() 14 | -------------------------------------------------------------------------------- /nodejs/testdata/check_certificate.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: nodejs 4 | command: "/nodejs/bin/node" 5 | args: ["/check_certificate.js"] 6 | expectedOutput: ['statusCode: 200'] 7 | -------------------------------------------------------------------------------- /nodejs/testdata/check_headers.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | fileExistenceTests: 3 | - name: npm 4 | path: '/nodejs/include/node/node.h' 5 | shouldExist: false 6 | -------------------------------------------------------------------------------- /nodejs/testdata/check_npm.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | fileExistenceTests: 3 | - name: npm 4 | path: '/nodejs/lib/node_modules/npm' 5 | shouldExist: false 6 | - name: corepack 7 | path: '/nodejs/lib/node_modules/corepack' 8 | shouldExist: false 9 | - name: npm 10 | path: '/nodejs/bin/npm' 11 | shouldExist: false 12 | - name: corepack 13 | path: '/nodejs/bin/corepack' 14 | shouldExist: false 15 | - name: npx 16 | path: '/nodejs/bin/npx' 17 | shouldExist: false 18 | -------------------------------------------------------------------------------- /nodejs/testdata/nodejs20.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: nodejs 4 | command: "/nodejs/bin/node" 5 | args: ["--version"] 6 | expectedOutput: ["v20.19.2"] 7 | -------------------------------------------------------------------------------- /nodejs/testdata/nodejs22.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: nodejs 4 | command: "/nodejs/bin/node" 5 | args: ["--version"] 6 | expectedOutput: ["v22.16.0"] 7 | -------------------------------------------------------------------------------- /nodejs/testdata/nodejs24.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "2.0.0" 2 | commandTests: 3 | - name: nodejs 4 | command: "/nodejs/bin/node" 5 | args: ["--version"] 6 | expectedOutput: ["v24.1.0"] 7 | -------------------------------------------------------------------------------- /private/extensions/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/distroless/4c40ba78166a7e559bcd41df2083d45bdb4212e7/private/extensions/BUILD.bazel -------------------------------------------------------------------------------- /private/extensions/busybox.bzl: -------------------------------------------------------------------------------- 1 | "busybox" 2 | 3 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") 4 | load("//experimental/busybox:commands.bzl", "BUSYBOX_ARCHIVE_BUILD") 5 | 6 | def _busybox_impl(module_ctx): 7 | mod = module_ctx.modules[0] 8 | 9 | if len(module_ctx.modules) > 1: 10 | fail("busybox.archive should be called only once") 11 | if not mod.is_root: 12 | fail("busybox.archive should be called from root module only.") 13 | 14 | # To update amd64 busybox binary (#1014) 15 | # Get the latest commit hash from dist-amd64 branch of docker-library repo. You can also view it 16 | # at https://github.com/docker-library/official-images/blob/master/library/busybox 17 | # Substitute it in the link: https://github.com/docker-library/busybox/raw//latest/musl/busybox.tar.xz 18 | # Update the sha256 value. Since github api doesn't give sha256 value, it can be obtained using sha256sum command. 19 | http_archive( 20 | name = "busybox_amd64", 21 | sha256 = "77b216d55c6895ddb04a90f3025b5ce2480140da779fe3dca91303b135a1fefe", 22 | urls = ["https://github.com/docker-library/busybox/raw/09ee80aedec1d8c604f104e8bec41ed19274620a/latest/musl/busybox.tar.xz"], 23 | build_file_content = BUSYBOX_ARCHIVE_BUILD, 24 | ) 25 | 26 | http_file( 27 | name = "busybox_arm", 28 | executable = True, 29 | sha256 = "cd04052b8b6885f75f50b2a280bfcbf849d8710c8e61d369c533acf307eda064", 30 | urls = ["https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-armv7l"], 31 | ) 32 | 33 | # To update arm64 busybox binary (#657) 34 | # Get the latest commit hash from dist-arm64v8 branch of docker-library repo. You can also view it 35 | # at https://github.com/docker-library/official-images/blob/master/library/busybox 36 | # Substitute it in the link: https://github.com/docker-library/busybox/raw//latest/musl/busybox.tar.xz 37 | # Update the sha256 value. Since github api doesn't give sha256 value, it can be obtained using sha256sum command. 38 | http_archive( 39 | name = "busybox_arm64", 40 | sha256 = "1d0610f348ae3f95897a967fae429b0a0c712b252ca63e1547a89bf13a1a82c7", 41 | urls = ["https://github.com/docker-library/busybox/raw/e5e22cb0710fe54da4beaa6a72c1bd56b8fc9c54/latest/musl/busybox.tar.xz"], 42 | build_file_content = BUSYBOX_ARCHIVE_BUILD, 43 | ) 44 | 45 | http_file( 46 | name = "busybox_s390x", 47 | executable = True, 48 | sha256 = "48d13ac057046b95ba58921958be639cc3a179ac888cdd65aacd7a69139aa857", 49 | urls = ["https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-s390x"], 50 | ) 51 | 52 | # To update ppc64le busybox binary (#723) 53 | # Get the latest commit hash from dist-ppc64le branch of docker-library repo. You can also view it 54 | # at https://github.com/docker-library/official-images/blob/master/library/busybox 55 | # Substitute it in the link: https://github.com/docker-library/busybox/raw//latest/musl/busybox.tar.xz 56 | # Update the sha256 value. Since github api doesn't give sha256 value, it can be obtained using sha256sum command. 57 | http_archive( 58 | name = "busybox_ppc64le", 59 | sha256 = "2d898cab234190697e5df74c537dd86955e9f61725d6c86d97b97c3d58aed9ca", 60 | urls = ["https://github.com/docker-library/busybox/raw/aa059e43d48801abcb012dfa965a432fa12c385d/latest/musl/busybox.tar.xz"], 61 | build_file_content = BUSYBOX_ARCHIVE_BUILD, 62 | ) 63 | 64 | return module_ctx.extension_metadata( 65 | root_module_direct_deps = [ 66 | "busybox_amd64", 67 | "busybox_arm", 68 | "busybox_arm64", 69 | "busybox_s390x", 70 | "busybox_ppc64le", 71 | ], 72 | root_module_direct_dev_deps = [], 73 | ) 74 | 75 | _archive = tag_class(attrs = {}) 76 | 77 | busybox = module_extension( 78 | implementation = _busybox_impl, 79 | tag_classes = { 80 | "archive": _archive, 81 | }, 82 | ) 83 | -------------------------------------------------------------------------------- /private/extensions/java.bzl: -------------------------------------------------------------------------------- 1 | "java" 2 | 3 | STATIC_MTREE = """\ 4 | etc/ssl/certs/ time=946684800.0 mode=755 gid=0 uid=0 type=dir 5 | etc/ssl/certs/java/ time=946684800.0 mode=755 gid=0 uid=0 type=dir 6 | usr/lib/jvm/ time=946684800.0 mode=755 gid=0 uid=0 type=dir 7 | # NOTE: cacerts is moved to ./etc/ssl/certs/java/cacerts via the awk mutation hence 8 | # a symlink created in the original location for completeness. 9 | usr/lib/jvm/%s/lib/security/cacerts nlink=0 time=946684800.0 mode=777 gid=0 uid=0 type=link link=/etc/ssl/certs/java/cacerts 10 | """ 11 | 12 | AWK = """\ 13 | { 14 | sub("^" "output/lib/security/cacerts", "./etc/ssl/certs/java/cacerts") 15 | sub("^" "output", "./usr/lib/jvm/%s") 16 | sub(/time=[0-9\\.]+/, "time=946684800.0"); 17 | if ($1 ~ ".*legal/.*" || $1 ~ ".*conf/.*") { 18 | # keep it as 0755 19 | # or 0644 if its a file 20 | if ($0 ~ ".*type=file.*") { 21 | sub("mode=0755", "mode=0644") 22 | } 23 | } else if ($1 ~ ".*\\.jsa") { 24 | sub("mode=0755", "mode=0644") 25 | } if ($0 ~ ".*type=dir.*") { 26 | # keep the 0755 permission override 27 | } else { 28 | sub("mode=0755", "") 29 | } 30 | # pkg_tar strips the leading ./ so we do too to avoid 31 | # `duplicates of file paths not supported` error 32 | sub("^" "./", "") 33 | print 34 | } 35 | """ 36 | 37 | BUILD_TMPL = """\ 38 | # GENERATED BY temurin_archive.bzl 39 | load("@distroless//private/pkg:debian_spdx.bzl", "debian_spdx") 40 | load("@distroless//private/util:merge_providers.bzl", "merge_providers") 41 | load("@aspect_bazel_lib//lib:tar.bzl", "tar", "mtree_spec") 42 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 43 | 44 | SRCS = glob(["output/**/*"]) 45 | mtree_spec( 46 | name = "mtree", 47 | srcs = SRCS, 48 | ) 49 | 50 | genrule( 51 | name = "mutate_mtree", 52 | srcs = [":mtree"], 53 | tools = ["static.mtree", "mutate.awk"], 54 | outs = ["out.mtree"], 55 | cmd = "cat $(execpath :static.mtree) >$@ && awk -f $(execpath :mutate.awk) <$< >>$@ && sort -o $@ $@" 56 | ) 57 | 58 | tar( 59 | name = "data", 60 | srcs = SRCS, 61 | mtree = "out.mtree" 62 | ) 63 | 64 | pkg_tar( 65 | name = "_control", 66 | srcs = ["control"], 67 | ) 68 | 69 | debian_spdx( 70 | name = "spdx", 71 | control = ":_control.tar", 72 | data = ":data.tar", 73 | package_name = "{package_name}", 74 | spdx_id = "{spdx_id}", 75 | sha256 = "{sha256}", 76 | urls = [{urls}] 77 | ) 78 | 79 | merge_providers( 80 | name = "{name}", 81 | srcs = [":data", ":spdx"], 82 | visibility = ["//visibility:public"], 83 | ) 84 | """ 85 | 86 | def _impl(rctx): 87 | name = rctx.attr.name.split("~")[-1] 88 | rctx.report_progress("Fetching {}".format(rctx.attr.package_name)) 89 | rctx.download_and_extract( 90 | url = rctx.attr.urls, 91 | sha256 = rctx.attr.sha256, 92 | stripPrefix = rctx.attr.strip_prefix, 93 | output = "output", 94 | ) 95 | rctx.file("static.mtree", STATIC_MTREE % name) 96 | rctx.file("mutate.awk", AWK % name) 97 | rctx.template( 98 | "control", 99 | rctx.attr.control, 100 | substitutions = { 101 | "{{VERSION}}": rctx.attr.version, 102 | "{{ARCHITECTURE}}": rctx.attr.architecture, 103 | "{{SHA256}}": rctx.attr.sha256, 104 | }, 105 | ) 106 | rctx.file( 107 | "BUILD.bazel", 108 | content = BUILD_TMPL.format( 109 | name = name, 110 | package_name = rctx.attr.package_name, 111 | version = rctx.attr.version, 112 | spdx_id = rctx.attr.name, 113 | urls = ",".join(['"%s"' % url for url in rctx.attr.urls]), 114 | sha256 = rctx.attr.sha256, 115 | ), 116 | ) 117 | 118 | temurin_archive = repository_rule( 119 | implementation = _impl, 120 | attrs = { 121 | "urls": attr.string_list(mandatory = True), 122 | "sha256": attr.string(mandatory = True), 123 | "strip_prefix": attr.string(), 124 | "package_name": attr.string(default = "temurin"), 125 | "version": attr.string(mandatory = True), 126 | "plain_version": attr.string(mandatory = True), 127 | "architecture": attr.string(mandatory = True), 128 | # control is only used to populate the sbom, see https://github.com/GoogleContainerTools/distroless/issues/1373 129 | # for why writing debian control files to the image is incompatible with scanners. 130 | "control": attr.label(), 131 | }, 132 | ) 133 | 134 | def _version_repo_impl(rctx): 135 | rctx.file( 136 | "versions.bzl", 137 | content = "JAVA_RELEASE_VERSIONS={}".format(rctx.attr.versions), 138 | ) 139 | rctx.file("BUILD.bazel", 'exports_files(["versions.bzl"])') 140 | 141 | version_repo = repository_rule( 142 | implementation = _version_repo_impl, 143 | attrs = { 144 | "versions": attr.string_dict(), 145 | }, 146 | ) 147 | 148 | def _java_impl(module_ctx): 149 | mod = module_ctx.modules[0] 150 | 151 | if len(module_ctx.modules) > 1: 152 | fail("java.archive should be called only once") 153 | if not mod.is_root: 154 | fail("java.archive should be called from root module only.") 155 | 156 | direct_deps = ["java_versions"] 157 | versions = {} 158 | 159 | for mod in module_ctx.modules: 160 | for archive in mod.tags.archive: 161 | direct_deps.append(archive.name) 162 | versions[archive.name] = archive.plain_version 163 | temurin_archive( 164 | name = archive.name, 165 | urls = archive.urls, 166 | sha256 = archive.sha256, 167 | strip_prefix = archive.strip_prefix, 168 | package_name = archive.package_name, 169 | version = archive.version, 170 | plain_version = archive.plain_version, 171 | architecture = archive.architecture, 172 | control = "//java:control", 173 | ) 174 | 175 | version_repo( 176 | name = "java_versions", 177 | versions = versions, 178 | ) 179 | 180 | return module_ctx.extension_metadata( 181 | root_module_direct_deps = direct_deps, 182 | root_module_direct_dev_deps = [], 183 | ) 184 | 185 | _archive = tag_class(attrs = { 186 | "name": attr.string(mandatory = True), 187 | "urls": attr.string_list(mandatory = True), 188 | "sha256": attr.string(mandatory = True), 189 | "strip_prefix": attr.string(), 190 | "package_name": attr.string(default = "temurin"), 191 | "version": attr.string(mandatory = True), 192 | "plain_version": attr.string(mandatory = True), 193 | "architecture": attr.string(mandatory = True), 194 | }) 195 | 196 | java = module_extension( 197 | implementation = _java_impl, 198 | tag_classes = { 199 | "archive": _archive, 200 | }, 201 | ) 202 | -------------------------------------------------------------------------------- /private/extensions/version.bzl: -------------------------------------------------------------------------------- 1 | "generates version information from lockfiles" 2 | 3 | # buildifier: disable=bzl-visibility 4 | load("@rules_distroless//apt/private:version.bzl", _version = "version") 5 | 6 | _VERSIONS_TMPL = """\ 7 | "versions repo" 8 | 9 | # AUTO GENERATED. DO NOT EDIT. 10 | _versions = {} 11 | 12 | # buildifier: disable=function-docstring 13 | def version(dist, arch, name): 14 | if dist not in _versions: 15 | fail("unknown dist {{}}".format(dist)) 16 | if name not in _versions[dist]: 17 | fail("unknown package {{}}".format(name)) 18 | if arch not in _versions[dist][name]: 19 | fail("unknown arch {{}}".format(arch)) 20 | return struct(**_versions[dist][name][arch]) 21 | """ 22 | 23 | def _parse_version(raw): 24 | (epoch, upstream, revision) = _version.parse(raw) 25 | 26 | return struct(raw = raw, epoch = epoch, upstream = upstream, revision = revision) 27 | 28 | def _version_repo_impl(rctx): 29 | rctx.file("versions.bzl", _VERSIONS_TMPL.format(json.decode(rctx.attr.versions))) 30 | rctx.file("BUILD.bazel", "exports_files(['versions.bzl'])") 31 | 32 | version_repo = repository_rule( 33 | implementation = _version_repo_impl, 34 | attrs = { 35 | "versions": attr.string(), 36 | }, 37 | ) 38 | 39 | def _version_impl(module_ctx): 40 | versions = dict() 41 | 42 | for mod in module_ctx.modules: 43 | for from_lock in mod.tags.from_lock: 44 | lock = json.decode(module_ctx.read(from_lock.lock)) 45 | repo = from_lock.repo_name 46 | 47 | if lock["version"] != 1: 48 | fail("unknown lock version") 49 | 50 | if repo not in versions: 51 | versions[repo] = dict() 52 | 53 | for pkg in lock["packages"]: 54 | if pkg["name"] not in versions[repo]: 55 | versions[repo][pkg["name"]] = dict() 56 | 57 | versions[repo][pkg["name"]][pkg["arch"]] = _parse_version(pkg["version"]) 58 | 59 | version_repo( 60 | name = "versions", 61 | versions = json.encode(versions), 62 | ) 63 | 64 | return module_ctx.extension_metadata( 65 | root_module_direct_deps = ["versions"], 66 | root_module_direct_dev_deps = [], 67 | ) 68 | 69 | _from_lock = tag_class(attrs = { 70 | "repo_name": attr.string(mandatory = True), 71 | "lock": attr.label(mandatory = True), 72 | }) 73 | 74 | version = module_extension( 75 | implementation = _version_impl, 76 | tag_classes = { 77 | "from_lock": _from_lock, 78 | }, 79 | ) 80 | -------------------------------------------------------------------------------- /private/oci/BUILD.bazel: -------------------------------------------------------------------------------- 1 | exports_files(["sign_and_push.sh.tpl"]) 2 | -------------------------------------------------------------------------------- /private/oci/cc_image.bzl: -------------------------------------------------------------------------------- 1 | load("@rules_oci//oci:defs.bzl", "oci_image") 2 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 3 | 4 | def cc_image(name, srcs, base): 5 | native.cc_binary( 6 | name = "%s_binary" % name, 7 | srcs = srcs, 8 | ) 9 | 10 | pkg_tar( 11 | name = "%s_layer" % name, 12 | srcs = [ 13 | ":%s_binary" % name, 14 | ], 15 | ) 16 | 17 | oci_image( 18 | name = name, 19 | base = base, 20 | entrypoint = [ 21 | "/%s_binary" % name, 22 | ], 23 | tars = [ 24 | ":%s_layer" % name, 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /private/oci/defs.bzl: -------------------------------------------------------------------------------- 1 | load(":java_image.bzl", _java_image = "java_image") 2 | load(":cc_image.bzl", _cc_image = "cc_image") 3 | load(":rust_image.bzl", _rust_image = "rust_image") 4 | load(":go_image.bzl", _go_image = "go_image") 5 | load(":sign_and_push.bzl", _sign_and_push_all = "sign_and_push_all") 6 | 7 | java_image = _java_image 8 | cc_image = _cc_image 9 | rust_image = _rust_image 10 | go_image = _go_image 11 | sign_and_push_all = _sign_and_push_all 12 | -------------------------------------------------------------------------------- /private/oci/digest.bzl: -------------------------------------------------------------------------------- 1 | "generate digest for oci_image and oci_image_index" 2 | 3 | load("@aspect_bazel_lib//lib:copy_file.bzl", "copy_file") 4 | load("@aspect_bazel_lib//lib:directory_path.bzl", "directory_path") 5 | load("@aspect_bazel_lib//lib:jq.bzl", "jq") 6 | 7 | # Normally we'd use the `.digest` target that rules_oci creates for every oci_image but 8 | # we also use oci_image_index which does not have a digest target. This was fixed in 9 | # https://github.com/bazel-contrib/rules_oci/pull/742 but it on the 2.x releases of rules_oci 10 | # TODO: Remove this once we upgrade to rules_oci 2.x 11 | def digest(name, image, **kwargs): 12 | # `oci_image_rule` and `oci_image_index_rule` produce a directory as default output. 13 | # Label for the [name]/index.json file 14 | directory_path( 15 | name = "_{}_index_json".format(name), 16 | directory = image, 17 | path = "index.json", 18 | **kwargs 19 | ) 20 | 21 | copy_file( 22 | name = "_{}_index_json_cp".format(name), 23 | src = "_{}_index_json".format(name), 24 | out = "_{}_index.json".format(name), 25 | **kwargs 26 | ) 27 | 28 | # Matches the [name].digest target produced by rules_docker container_image 29 | jq( 30 | name = name, 31 | args = ["--raw-output"], 32 | srcs = ["_{}_index.json".format(name)], 33 | filter = """.manifests[0].digest""", 34 | out = name + ".json.sha256", # path chosen to match rules_docker for easy migration 35 | **kwargs 36 | ) 37 | -------------------------------------------------------------------------------- /private/oci/go_image.bzl: -------------------------------------------------------------------------------- 1 | load("@rules_go//go:def.bzl", "go_binary") 2 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 3 | load("@rules_oci//oci:defs.bzl", "oci_image") 4 | 5 | def go_image(name, srcs, base, arch = "amd64", os = "linux"): 6 | go_binary( 7 | name = "{}_binary".format(name), 8 | srcs = srcs, 9 | goarch = arch, 10 | goos = os, 11 | pure = "on", 12 | ) 13 | 14 | pkg_tar( 15 | name = "{}_layer".format(name), 16 | srcs = ["{}_binary".format(name)], 17 | ) 18 | 19 | oci_image( 20 | name = name, 21 | base = base, 22 | entrypoint = ["/{}_binary".format(name)], 23 | tars = [ 24 | "{}_layer".format(name), 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /private/oci/java_image.bzl: -------------------------------------------------------------------------------- 1 | load("@rules_oci//oci:defs.bzl", "oci_image") 2 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 3 | 4 | def java_image(name, srcs, main_class, base): 5 | native.java_binary( 6 | name = "%s_binary" % name, 7 | srcs = srcs, 8 | main_class = main_class, 9 | ) 10 | 11 | pkg_tar( 12 | name = "%s_layer" % name, 13 | srcs = [ 14 | ":%s_binary" % name, 15 | ], 16 | ) 17 | 18 | oci_image( 19 | name = name, 20 | base = base, 21 | cmd = [ 22 | "/%s_binary.jar" % name, 23 | ], 24 | tars = [ 25 | ":%s_layer" % name, 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /private/oci/rust_image.bzl: -------------------------------------------------------------------------------- 1 | load("@rules_oci//oci:defs.bzl", "oci_image") 2 | load("@rules_rust//rust:defs.bzl", "rust_binary") 3 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 4 | 5 | def rust_image(name, srcs, base, tags): 6 | rust_binary( 7 | name = "%s_binary" % name, 8 | srcs = srcs, 9 | tags = tags, 10 | ) 11 | 12 | pkg_tar( 13 | name = "%s_layer" % name, 14 | srcs = [ 15 | ":%s_binary" % name, 16 | ], 17 | tags = tags, 18 | ) 19 | 20 | oci_image( 21 | name = name, 22 | base = base, 23 | entrypoint = [ 24 | "/%s_binary" % name, 25 | ], 26 | tars = [ 27 | ":%s_layer" % name, 28 | ], 29 | tags = tags, 30 | ) 31 | -------------------------------------------------------------------------------- /private/oci/sign_and_push.bzl: -------------------------------------------------------------------------------- 1 | "rules for signing, attesting and pushing images" 2 | 3 | load("@bazel_skylib//rules:write_file.bzl", "write_file") 4 | load("//private/pkg:oci_image_spdx.bzl", "oci_image_spdx") 5 | load(":digest.bzl", "digest") 6 | 7 | PUSH_AND_SIGN_CMD = """\ 8 | # Push {IMAGE} 9 | repository="$(stamp "{REPOSITORY}")" 10 | tag="$(stamp "{TAG}")" 11 | digest="$(cat {DIGEST})" 12 | echo "Pushing $repository@$digest" 13 | {CRANE} push {IMAGE} "$repository@$digest" 14 | {COSIGN} attest "$repository@$digest" --predicate "{SBOM}" --type "spdx" --yes 15 | {COSIGN} sign "$repository@$digest" --yes 16 | {CRANE} tag "$repository@$digest" "$tag" 17 | """ 18 | 19 | TAG_CMD = """\ 20 | # Tag {IMAGE} 21 | from="$(stamp "{FROM}")" 22 | tag="$(stamp "{TAG}")" 23 | {CRANE} tag "$from" "$tag" 24 | """ 25 | 26 | def _sign_and_push_impl(ctx): 27 | cmds = [] 28 | 29 | runfiles = ctx.runfiles(files = ctx.files.targets + [ctx.version_file, ctx.file._crane, ctx.file._cosign]) 30 | 31 | for (image, target) in ctx.attr.targets.items(): 32 | files = target[DefaultInfo].files.to_list() 33 | 34 | all_refs = ctx.attr.refs[image] 35 | for ref in all_refs: 36 | repository_and_tag = ref.split(":") 37 | cmds.append( 38 | PUSH_AND_SIGN_CMD.format( 39 | IMAGE = files[0].short_path, 40 | SBOM = files[1].short_path, 41 | DIGEST = files[2].short_path, 42 | CRANE = ctx.file._crane.short_path, 43 | COSIGN = ctx.file._cosign.short_path, 44 | REPOSITORY = repository_and_tag[0], 45 | TAG = repository_and_tag[1], 46 | ), 47 | ) 48 | 49 | for tag in ctx.attr.more_tags[ref]: 50 | cmds.append( 51 | TAG_CMD.format( 52 | IMAGE = image, 53 | FROM = ref, 54 | TAG = tag, 55 | CRANE = ctx.file._crane.short_path, 56 | ), 57 | ) 58 | 59 | executable = ctx.actions.declare_file("{}_sign_and_push.sh".format(ctx.label.name)) 60 | ctx.actions.expand_template( 61 | template = ctx.file._push_tpl, 62 | output = executable, 63 | substitutions = { 64 | "{{VERSION_FILE}}": ctx.version_file.short_path, 65 | "{{CMDS}}": "\n".join(cmds), 66 | }, 67 | is_executable = True, 68 | ) 69 | 70 | return DefaultInfo(executable = executable, runfiles = runfiles) 71 | 72 | sign_and_push = rule( 73 | implementation = _sign_and_push_impl, 74 | attrs = { 75 | "refs": attr.string_list_dict(mandatory = True), 76 | "targets": attr.string_keyed_label_dict(mandatory = True, cfg = "exec"), 77 | "more_tags": attr.string_list_dict(mandatory = True), 78 | "_push_tpl": attr.label(default = "sign_and_push.sh.tpl", allow_single_file = True), 79 | "_crane": attr.label(allow_single_file = True, cfg = "exec", default = "@oci_crane_toolchains//:current_toolchain"), 80 | "_cosign": attr.label(allow_single_file = True, cfg = "exec", default = "@oci_cosign_toolchains//:current_toolchain"), 81 | }, 82 | executable = True, 83 | ) 84 | 85 | def sign_and_push_all(name, images): 86 | """simple macro singing and pushing images 87 | 88 | Args: 89 | name: name of the target 90 | images: a dict where keys are fully qualified image reference and values are image label 91 | """ 92 | 93 | # since bazel doesn't allow dicts of dicts of lists as attrs, we have to write this nutty 94 | # deduplication code 95 | dedup_image_dict = dict() 96 | dedup_more_tags = dict() 97 | dedup_push_dict = dict() 98 | 99 | for (idx, (ref, image)) in enumerate(images.items()): 100 | repository_and_tag = ref.split(":") 101 | repository = repository_and_tag[0] 102 | tag = repository_and_tag[1] 103 | if image in dedup_image_dict: 104 | foundPrefix = False 105 | for oRef in dedup_image_dict[image]: 106 | if oRef.split(":")[0] == repository: 107 | dedup_more_tags[oRef].append(tag) 108 | foundPrefix = True 109 | break 110 | if not foundPrefix: 111 | dedup_image_dict[image].append(ref) 112 | dedup_more_tags[ref] = [] 113 | else: 114 | dedup_image_dict[image] = [ref] 115 | dedup_more_tags[ref] = [] 116 | 117 | for (idx, (image, _)) in enumerate(dedup_image_dict.items()): 118 | oci_image_spdx( 119 | name = "{}_{}_sbom".format(name, idx), 120 | image = image, 121 | ) 122 | digest( 123 | name = "{}_{}_digest".format(name, idx), 124 | image = image, 125 | ) 126 | 127 | native.filegroup( 128 | name = "{}_{}".format(name, idx), 129 | srcs = [ 130 | image, 131 | ":{}_{}_sbom".format(name, idx), 132 | ":{}_{}_digest".format(name, idx), 133 | ], 134 | ) 135 | 136 | dedup_push_dict[image] = "{}_{}".format(name, idx) 137 | 138 | write_file( 139 | name = name + ".query", 140 | content = [ 141 | "{repo} {image}".format( 142 | repo = repo, 143 | image = image, 144 | ) 145 | for (image, refs) in dedup_image_dict.items() 146 | for repo in refs 147 | ], 148 | out = name + "_query", 149 | ) 150 | 151 | sign_and_push( 152 | name = name, 153 | targets = dedup_push_dict, 154 | more_tags = dedup_more_tags, 155 | refs = dedup_image_dict, 156 | ) 157 | -------------------------------------------------------------------------------- /private/oci/sign_and_push.sh.tpl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | KEYLESS="${KEYLESS:-}" 5 | 6 | while (( $# > 0 )); do 7 | case $1 in 8 | (--keyless) 9 | KEYLESS="$2" 10 | shift 11 | shift;; 12 | (*) 13 | echo "unknown arg $1" 14 | exit 1 15 | esac 16 | done 17 | 18 | if [ -z $KEYLESS ]; then 19 | echo "--keyless flag or KEYLESS environment variable must be provided" 20 | exit 1 21 | fi 22 | 23 | echo "## Signing and pushing images..." 24 | echo "" 25 | 26 | readonly version_file="$(cat "{{VERSION_FILE}}")" 27 | 28 | function stamp() { 29 | local str=$1 30 | while read -r line; 31 | do 32 | IFS=" " read -r key value <<< "$line" 33 | str="${str/\{$key\}/$value}" 34 | done <<< "$version_file" 35 | echo "$str" 36 | } 37 | 38 | 39 | export GOOGLE_SERVICE_ACCOUNT_NAME="${KEYLESS}" 40 | 41 | {{CMDS}} 42 | 43 | echo "" 44 | echo "👌 Finished pushing & signing" 45 | echo "" 46 | -------------------------------------------------------------------------------- /private/pkg/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_go//go:def.bzl", "go_binary") 2 | 3 | package(default_visibility = ["//visibility:public"]) 4 | 5 | go_binary( 6 | name = "oci_image_spdx", 7 | srcs = ["oci_image_spdx.go"], 8 | deps = [ 9 | "@com_github_spdx_tools_golang//json", 10 | "@com_github_spdx_tools_golang//spdx/common", 11 | "@com_github_spdx_tools_golang//spdx/v2_3", 12 | ], 13 | ) 14 | 15 | go_binary( 16 | name = "debian_spdx", 17 | srcs = ["debian_spdx.go"], 18 | deps = [ 19 | "@com_github_spdx_tools_golang//json", 20 | "@com_github_spdx_tools_golang//spdx/common", 21 | "@com_github_spdx_tools_golang//spdx/v2_3", 22 | ], 23 | ) 24 | -------------------------------------------------------------------------------- /private/pkg/debian_spdx.bzl: -------------------------------------------------------------------------------- 1 | SPDX_CMD = """\ 2 | tmp="$(mktemp -d)" 3 | 4 | tar -xf "$1" -C "$tmp" "./control" || tar -xf "$1" -C "$tmp" "control" 5 | 6 | if tar -xf "$2" -C "$tmp" "usr/share/doc/$3/copyright" >/dev/null 2>&1; then 7 | COPYRIGHT="$tmp/usr/share/doc/$3/copyright" 8 | fi 9 | 10 | if tar -xf "$2" -C "$tmp" "./usr/share/doc/$3/copyright" >/dev/null 2>&1; then 11 | COPYRIGHT="$tmp/usr/share/doc/$3/copyright" 12 | fi 13 | shift 14 | shift 15 | shift 16 | {generator} --control="$tmp/control" --copyright=$COPYRIGHT $@ 17 | """ 18 | 19 | def _impl(ctx): 20 | output = ctx.actions.declare_file("%s.spdx.json" % ctx.label.name) 21 | 22 | args = ctx.actions.args() 23 | args.add(ctx.file.control.path) 24 | args.add(ctx.file.data.path) 25 | args.add(ctx.attr.package_name) 26 | args.add(ctx.attr.spdx_id, format = "--id=%s") 27 | args.add(output.path, format = "--output=%s") 28 | args.add(ctx.label, format = "--generates=%s") 29 | 30 | # TODO: multiple urls. it is not required at the moment since .deb are fetched without a fallback mirror. 31 | args.add(ctx.attr.urls[0], format = "--url=%s") 32 | args.add(ctx.attr.sha256, format = "--sha256=%s") 33 | 34 | ctx.actions.run_shell( 35 | inputs = [ctx.file.control, ctx.file.data], 36 | outputs = [output], 37 | command = SPDX_CMD.format(generator = ctx.file._generator.path), 38 | tools = [ctx.executable._generator], 39 | arguments = [args], 40 | ) 41 | 42 | return OutputGroupInfo( 43 | spdx = depset([output]), 44 | ) 45 | 46 | debian_spdx = rule( 47 | implementation = _impl, 48 | attrs = { 49 | "control": attr.label(mandatory = True, allow_single_file = [".tar", ".tar.xz", "tar.gz"]), 50 | "data": attr.label(mandatory = True, allow_single_file = [".tar", ".tar.xz", "tar.gz"]), 51 | "package_name": attr.string(mandatory = True), 52 | "spdx_id": attr.string(mandatory = True), 53 | "urls": attr.string_list(mandatory = True), 54 | "sha256": attr.string(mandatory = True), 55 | "_generator": attr.label(default = ":debian_spdx", executable = True, allow_single_file = True, cfg = "exec"), 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /private/pkg/debian_spdx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "regexp" 11 | "strings" 12 | 13 | spdx_json "github.com/spdx/tools-golang/json" 14 | "github.com/spdx/tools-golang/spdx/common" 15 | "github.com/spdx/tools-golang/spdx/v2_3" 16 | ) 17 | 18 | func pkgId(id string) string { 19 | id = strings.ReplaceAll(id, "/", "-slash-") 20 | id = strings.ReplaceAll(id, "_", "-underscore-") 21 | id = strings.ReplaceAll(id, "_", "-underscore-") 22 | id = strings.ReplaceAll(id, ":", "-colon-") 23 | id = strings.ReplaceAll(id, "@", "-at-") 24 | return id 25 | } 26 | 27 | // TODO: make part of debian_package_manager 28 | func parseDebControl(r io.Reader) (map[string]string, error) { 29 | const ( 30 | separator = ":" 31 | ) 32 | var currentKey string 33 | var currentEntry = map[string]string{} 34 | 35 | continuation := regexp.MustCompile(`^\s`) 36 | s := bufio.NewScanner(r) 37 | 38 | // some Packages.xz lines are super big 39 | maxCap := 1024 * 1024 40 | megaBuffer := make([]byte, maxCap) 41 | s.Buffer(megaBuffer, maxCap) 42 | 43 | ln := 0 44 | for s.Scan() { 45 | line := s.Text() 46 | ln++ 47 | 48 | if continuation.MatchString(line) { 49 | if currentKey == "" || len(currentEntry) == 0 { 50 | return nil, fmt.Errorf("bad indentation on line %d: %q", ln, line) 51 | } 52 | currentEntry[currentKey] += "\n" + strings.TrimSpace(line) 53 | } else if strings.Contains(line, separator) { 54 | sp := strings.SplitN(line, separator, 2) 55 | currentKey = strings.TrimSpace(sp[0]) 56 | if _, ok := currentEntry[currentKey]; ok { 57 | return nil, fmt.Errorf("duplicate key %q on line %d: %q", currentKey, ln, line) 58 | } 59 | currentEntry[currentKey] = strings.TrimSpace(sp[1]) 60 | } else { 61 | return nil, fmt.Errorf("no indentation or delimiter on line %d: %q", ln, line) 62 | } 63 | 64 | } 65 | if s.Err() != nil { 66 | return nil, s.Err() 67 | } 68 | 69 | return currentEntry, nil 70 | } 71 | 72 | func main() { 73 | var control, output, sha256, url, id, copyright, generates string 74 | flag.StringVar(&control, "control", "", "") 75 | flag.StringVar(&output, "output", "", "") 76 | flag.StringVar(&sha256, "sha256", "", "") 77 | flag.StringVar(&id, "id", "", "") 78 | flag.StringVar(©right, "copyright", "", "") 79 | flag.StringVar(&generates, "generates", "", "") 80 | // TODO: multiple urls. it is not required at the moment since .deb are fetched without a fallback mirror. 81 | flag.StringVar(&url, "url", "", "") 82 | flag.Parse() 83 | 84 | read, err := os.Open(control) 85 | if err != nil { 86 | log.Fatalln(err) 87 | } 88 | mp, err := parseDebControl(read) 89 | if err != nil { 90 | log.Fatalln(err) 91 | } 92 | 93 | copyrightText := "NOASSERTION" 94 | if copyright != "" { 95 | copyrightBytes, err := os.ReadFile(copyright) 96 | if err != nil { 97 | log.Fatalln(err) 98 | } 99 | copyrightText = string(copyrightBytes) 100 | } 101 | 102 | supplier := &common.Supplier{} 103 | if mp["Maintainer"] != "" { 104 | supplier.Supplier = mp["Maintainer"] 105 | supplier.SupplierType = "Person" 106 | } 107 | 108 | deb := &v2_3.Package{ 109 | PackageSPDXIdentifier: common.ElementID(pkgId(id)), 110 | PackageName: mp["Package"], 111 | PackageDescription: mp["Description"], 112 | PackageSummary: strings.Split(mp["Description"], "\n")[0], 113 | PackageVersion: mp["Version"], 114 | PackageSourceInfo: mp["Source"], 115 | PackageHomePage: mp["Homepage"], 116 | BuiltDate: mp["Date"], 117 | ReleaseDate: mp["Date"], 118 | PackageDownloadLocation: url, 119 | PackageSupplier: supplier, 120 | PackageCopyrightText: copyrightText, 121 | PackageChecksums: []common.Checksum{ 122 | { 123 | Algorithm: common.SHA256, 124 | Value: sha256, 125 | }, 126 | }, 127 | PackageExternalReferences: []*v2_3.PackageExternalReference{ 128 | { 129 | 130 | Category: "PACKAGE-MANAGER", 131 | Locator: fmt.Sprintf("pkg:deb/debian/%s@%s?arch=%s", mp["Package"], mp["Version"], mp["Architecture"]), 132 | RefType: common.TypePackageManagerPURL, 133 | }, 134 | }, 135 | } 136 | gen := &v2_3.Package{ 137 | PackageSPDXIdentifier: common.ElementID(pkgId(generates)), 138 | PackageName: generates, 139 | PackageDescription: fmt.Sprintf("Generated from %s@%s", deb.PackageName, deb.PackageVersion), 140 | PackageDownloadLocation: "NOASSERTION", 141 | PackageCopyrightText: "NOASSERTION", 142 | } 143 | doc := &v2_3.Document{ 144 | SPDXIdentifier: gen.PackageSPDXIdentifier, 145 | Packages: []*v2_3.Package{ 146 | deb, 147 | gen, 148 | }, 149 | Relationships: []*v2_3.Relationship{ 150 | &v2_3.Relationship{ 151 | RefA: common.DocElementID{ElementRefID: gen.PackageSPDXIdentifier}, 152 | RefB: common.DocElementID{ElementRefID: deb.PackageSPDXIdentifier}, 153 | Relationship: common.TypeRelationshipGeneratedFrom, 154 | }, 155 | }, 156 | } 157 | outputFile, err := os.Create(output) 158 | if err != nil { 159 | log.Fatalln(err) 160 | } 161 | if err := spdx_json.Save2_3(doc, outputFile); err != nil { 162 | log.Fatalln(err) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /private/pkg/oci_image_spdx.bzl: -------------------------------------------------------------------------------- 1 | "an aspect rule that generates spdx by collecting spdx output group" 2 | 3 | def _image_aspect_impl(target, ctx): 4 | output = ctx.actions.declare_file("%s.spdx.json" % target.label.name) 5 | 6 | args = ctx.actions.args() 7 | args.add(output.path, format = "--output=%s") 8 | args.add(target.label, format = "--label=%s") 9 | 10 | input_depsets = [] 11 | 12 | if ctx.rule.kind == "oci_image": 13 | if ctx.rule.attr.base != None: 14 | output_group_info = ctx.rule.attr.base[OutputGroupInfo] 15 | if output_group_info and "spdx" in output_group_info: 16 | input_depsets.append(output_group_info.spdx) 17 | args.add_all(output_group_info.spdx, format_each = "--contains=%s") 18 | 19 | for src in ctx.rule.attr.tars: 20 | if OutputGroupInfo in src and "spdx" in src[OutputGroupInfo]: 21 | input_depsets.append(src[OutputGroupInfo].spdx) 22 | args.add_all(src[OutputGroupInfo].spdx) 23 | 24 | elif ctx.rule.kind == "oci_image_index": 25 | for image in ctx.rule.attr.images: 26 | if OutputGroupInfo in image and "spdx" in image[OutputGroupInfo]: 27 | input_depsets.append(image[OutputGroupInfo].spdx) 28 | args.add_all(image[OutputGroupInfo].spdx, format_each = "--contains=%s") 29 | 30 | ctx.actions.run( 31 | inputs = depset(transitive = input_depsets), 32 | outputs = [output], 33 | executable = ctx.executable._generator, 34 | arguments = [args], 35 | ) 36 | 37 | return [ 38 | OutputGroupInfo( 39 | spdx = depset([output]), 40 | ), 41 | ] 42 | 43 | image_aspect = aspect( 44 | implementation = _image_aspect_impl, 45 | attr_aspects = ["base", "images"], 46 | attrs = {"_generator": attr.label(default = ":oci_image_spdx", executable = True, allow_single_file = True, cfg = "exec")}, 47 | ) 48 | 49 | def _oci_image_spdx_impl(ctx): 50 | return DefaultInfo(files = ctx.attr.image[OutputGroupInfo].spdx) 51 | 52 | oci_image_spdx = rule( 53 | implementation = _oci_image_spdx_impl, 54 | attrs = { 55 | "image": attr.label(aspects = [image_aspect]), 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /private/pkg/oci_image_spdx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | spdx_json "github.com/spdx/tools-golang/json" 11 | "github.com/spdx/tools-golang/spdx/common" 12 | "github.com/spdx/tools-golang/spdx/v2_3" 13 | ) 14 | 15 | func pkgId(id string) string { 16 | id = strings.ReplaceAll(id, "/", "-slash-") 17 | id = strings.ReplaceAll(id, "_", "-underscore-") 18 | id = strings.ReplaceAll(id, "_", "-underscore-") 19 | id = strings.ReplaceAll(id, ":", "-colon-") 20 | id = strings.ReplaceAll(id, "@", "-at-") 21 | return id 22 | } 23 | 24 | type ArrayFlags []string 25 | 26 | func (i *ArrayFlags) String() string { 27 | return "my string representation" 28 | } 29 | 30 | func (i *ArrayFlags) Set(value string) error { 31 | *i = append(*i, value) 32 | return nil 33 | } 34 | 35 | func main() { 36 | var label, output string 37 | var contains ArrayFlags 38 | flag.StringVar(&output, "output", "", "") 39 | flag.StringVar(&label, "label", "", "") 40 | flag.Var(&contains, "contains", "") 41 | flag.Parse() 42 | 43 | id := pkgId(label) 44 | doc := v2_3.Document{ 45 | SPDXIdentifier: "SPDXRef-DOCUMENT", 46 | SPDXVersion: "SPDX-2.3", 47 | DocumentName: label, 48 | DataLicense: "CC0-1.0", 49 | DocumentNamespace: "http://spdx.org/spdxdocs/distroless/" + id, 50 | CreationInfo: &v2_3.CreationInfo{ 51 | Created: "1970-01-01T00:00:00Z", 52 | Creators: []common.Creator{ 53 | common.Creator{ 54 | Creator: "distroless", CreatorType: "Organization", 55 | }, 56 | }, 57 | LicenseListVersion: "NOASSERTION", 58 | }, 59 | Packages: []*v2_3.Package{ 60 | { 61 | PackageSPDXIdentifier: common.ElementID(id), 62 | PackageName: label, 63 | PackageDownloadLocation: "NOASSERTION", 64 | PackageCopyrightText: "NOASSERTION", 65 | }, 66 | }, 67 | Relationships: []*v2_3.Relationship{ 68 | { 69 | RefA: common.MakeDocElementID("", "DOCUMENT"), 70 | RefB: common.MakeDocElementID("", id), 71 | Relationship: "DESCRIBES", 72 | }, 73 | }, 74 | } 75 | spdxs := flag.Args() 76 | for _, p := range spdxs { 77 | bytes, err := os.Open(p) 78 | if err != nil { 79 | log.Fatalln(err) 80 | } 81 | subdoc, err := spdx_json.Load2_3(bytes) 82 | if err != nil { 83 | log.Fatalln(err) 84 | } 85 | doc.Packages = append(doc.Packages, subdoc.Packages...) 86 | for _, rel := range subdoc.Relationships { 87 | if rel.Relationship == "DESCRIBES" && rel.RefA.ElementRefID == "DOCUMENT" { 88 | continue 89 | } 90 | doc.Relationships = append(doc.Relationships, rel) 91 | } 92 | doc.Relationships = append(doc.Relationships, &v2_3.Relationship{ 93 | RefA: common.MakeDocElementID("", id), 94 | RefB: common.DocElementID{ElementRefID: subdoc.SPDXIdentifier}, 95 | Relationship: "DEPENDS_ON", 96 | }) 97 | } 98 | 99 | for _, contain := range contains { 100 | bytes, err := os.Open(contain) 101 | if err != nil { 102 | log.Fatalln(err) 103 | } 104 | subdoc, err := spdx_json.Load2_3(bytes) 105 | if err != nil { 106 | log.Fatalln(err) 107 | } 108 | doc.Packages = append(doc.Packages, subdoc.Packages...) 109 | 110 | var describedBy *common.DocElementID = nil 111 | for _, rel := range subdoc.Relationships { 112 | if rel.Relationship == "DESCRIBES" && rel.RefA.ElementRefID == "DOCUMENT" { 113 | if rel.RefA.ElementRefID == subdoc.SPDXIdentifier { 114 | describedBy = &rel.RefB 115 | } 116 | continue 117 | } 118 | doc.Relationships = append(doc.Relationships, rel) 119 | } 120 | if describedBy == nil { 121 | log.Fatalln(fmt.Errorf("%s: can not determine which package describes %s", contain, subdoc.SPDXIdentifier)) 122 | } 123 | doc.Relationships = append(doc.Relationships, &v2_3.Relationship{ 124 | RefA: common.MakeDocElementID("", id), 125 | RefB: *describedBy, 126 | Relationship: "CONTAINS", 127 | }) 128 | } 129 | 130 | outputFile, err := os.Create(output) 131 | if err != nil { 132 | log.Fatalln(err) 133 | } 134 | 135 | if err := spdx_json.Save2_3(&doc, outputFile); err != nil { 136 | log.Fatalln(err) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /private/pkg/test/oci_image/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_oci//oci:defs.bzl", "oci_image", "oci_image_index") 2 | load("@aspect_bazel_lib//lib:write_source_files.bzl", "write_source_files") 3 | load("//private/pkg:oci_image_spdx.bzl", "oci_image_spdx") 4 | load("//private/util:deb.bzl", "deb") 5 | 6 | oci_image( 7 | name = "image_arm64", 8 | architecture = "arm64", 9 | os = "linux", 10 | tars = [ 11 | deb.package("arm64", "debian12", "base-files"), 12 | ], 13 | ) 14 | 15 | oci_image_spdx( 16 | name = "image_arm64_sbom", 17 | image = ":image_arm64", 18 | ) 19 | 20 | oci_image( 21 | name = "image_amd64", 22 | architecture = "amd64", 23 | os = "linux", 24 | tars = [ 25 | deb.package("amd64", "debian12", "netbase"), 26 | ], 27 | ) 28 | 29 | oci_image_spdx( 30 | name = "image_amd64_sbom", 31 | image = ":image_amd64", 32 | ) 33 | 34 | oci_image_index( 35 | name = "fat_image", 36 | images = [ 37 | ":image_arm64", 38 | ":image_amd64", 39 | ], 40 | ) 41 | 42 | oci_image_spdx( 43 | name = "fat_image_sbom", 44 | image = ":fat_image", 45 | ) 46 | 47 | write_source_files( 48 | name = "test_sboms", 49 | files = { 50 | "fat_image_sbom.spdx.json": ":fat_image_sbom", 51 | "image_arm64.spdx.json": ":image_arm64_sbom", 52 | "image_amd64.spdx.json": ":image_amd64_sbom", 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /private/pkg/test/oci_image/fat_image_sbom.spdx.json: -------------------------------------------------------------------------------- 1 | {"spdxVersion":"SPDX-2.3","dataLicense":"CC0-1.0","SPDXID":"SPDXRef-DOCUMENT","name":"//private/pkg/test/oci_image:fat_image","documentNamespace":"http://spdx.org/spdxdocs/distroless/-slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-fat-underscore-image","creationInfo":{"licenseListVersion":"NOASSERTION","creators":["Organization: distroless"],"created":"1970-01-01T00:00:00Z"},"packages":[{"name":"//private/pkg/test/oci_image:fat_image","SPDXID":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-fat-underscore-image","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION"},{"name":"//private/pkg/test/oci_image:image_arm64","SPDXID":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-arm64","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION"},{"name":"base-files","SPDXID":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-base-files-underscore-12.4-p-deb12u11-underscore-arm64","versionInfo":"12.4+deb12u11","supplier":"Person: Santiago Vila \\\\u003csanvila@debian.org\\\\u003e","downloadLocation":"https://snapshot.debian.org/archive/debian/20250517T203043Z/pool/main/b/base-files/base-files_12.4+deb12u11_arm64.deb","checksums":[{"algorithm":"SHA256","checksumValue":"d758534164a8b6d0fc30407f81175e70a8035ba75853abd70311a6fcd3294822"}],"copyrightText":"This is the Debian prepackaged version of the Debian Base System\nMiscellaneous files. These files were written by Ian Murdock\n\u003cimurdock@debian.org\u003e and Bruce Perens \u003cbruce@pixar.com\u003e.\n\nThis package was first put together by Bruce Perens \u003cBruce@Pixar.com\u003e,\nfrom his own sources.\n\nThe GNU Public Licenses in /usr/share/common-licenses were taken from\nftp.gnu.org and are copyrighted by the Free Software Foundation, Inc.\n\nThe Artistic License in /usr/share/common-licenses is the one coming\nfrom Perl and its SPDX name is \"Artistic License 1.0 (Perl)\".\n\n\nCopyright (C) 1995-2011 Software in the Public Interest.\n\nThis program is free software; you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 2 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nOn Debian systems, the complete text of the GNU General\nPublic License can be found in `/usr/share/common-licenses/GPL'.\n","summary":"Debian base system miscellaneous files","description":"Debian base system miscellaneous files\nThis package contains the basic filesystem hierarchy of a Debian system, and\nseveral important miscellaneous files, such as /etc/debian_version,\n/etc/host.conf, /etc/issue, /etc/motd, /etc/profile, and others,\nand the text of several common licenses in use on Debian systems.","externalRefs":[{"referenceCategory":"PACKAGE-MANAGER","referenceType":"purl","referenceLocator":"pkg:deb/debian/base-files@12.4+deb12u11?arch=arm64"}]},{"name":"@bookworm//base-files/arm64:spdx","SPDXID":"SPDXRef--at-bookworm-slash--slash-base-files-slash-arm64-colon-spdx","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION","description":"Generated from base-files@12.4+deb12u11"},{"name":"//private/pkg/test/oci_image:image_amd64","SPDXID":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-amd64","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION"},{"name":"netbase","SPDXID":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-netbase-underscore-6.4-underscore-amd64","versionInfo":"6.4","supplier":"Person: Marco d'Itri \\\\u003cmd@linux.it\\\\u003e","downloadLocation":"https://snapshot.debian.org/archive/debian/20250517T203043Z/pool/main/n/netbase/netbase_6.4_all.deb","checksums":[{"algorithm":"SHA256","checksumValue":"29b23c48c0fe6f878e56c5ddc9f65d1c05d729360f3690a593a8c795031cd867"}],"copyrightText":"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nComment:\n This package was created by Peter Tobias tobias@et-inf.fho-emden.de on\n Wed, 24 Aug 1994 21:33:28 +0200 and maintained by Anthony Towns\n \u003cajt@debian.org\u003e until 2001.\n It is currently maintained by Marco d'Itri \u003cmd@linux.it\u003e.\n\nFiles: *\nCopyright:\n Copyright (c) 1994-1998 Peter Tobias\n Copyright (c) 1998-2001 Anthony Towns\n Copyright (c) 2002-2022 Marco d'Itri\nLicense: GPL-2\n This program is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License, version 2, as\n published by the Free Software Foundation.\n .\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNU General Public License for more details.\n .\n You should have received a copy of the GNU General Public License along\n with this program; if not, write to the Free Software Foundation,\n Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n .\n On Debian systems, the complete text of the GNU General Public License\n version 2 can be found in '/usr/share/common-licenses/GPL-2'.\n","summary":"Basic TCP/IP networking system","description":"Basic TCP/IP networking system\nThis package provides the necessary infrastructure for basic TCP/IP based\nnetworking.\n.\nIn particular, it supplies common name-to-number mappings in /etc/services,\n/etc/rpc, /etc/protocols and /etc/ethertypes.","externalRefs":[{"referenceCategory":"PACKAGE-MANAGER","referenceType":"purl","referenceLocator":"pkg:deb/debian/netbase@6.4?arch=all"}]},{"name":"@bookworm//netbase/amd64:spdx","SPDXID":"SPDXRef--at-bookworm-slash--slash-netbase-slash-amd64-colon-spdx","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION","description":"Generated from netbase@6.4"}],"relationships":[{"spdxElementId":"SPDXRef-DOCUMENT","relatedSpdxElement":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-fat-underscore-image","relationshipType":"DESCRIBES"},{"spdxElementId":"SPDXRef--at-bookworm-slash--slash-base-files-slash-arm64-colon-spdx","relatedSpdxElement":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-base-files-underscore-12.4-p-deb12u11-underscore-arm64","relationshipType":"GENERATED_FROM"},{"spdxElementId":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-arm64","relatedSpdxElement":"SPDXRef--at-bookworm-slash--slash-base-files-slash-arm64-colon-spdx","relationshipType":"DEPENDS_ON"},{"spdxElementId":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-fat-underscore-image","relatedSpdxElement":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-arm64","relationshipType":"CONTAINS"},{"spdxElementId":"SPDXRef--at-bookworm-slash--slash-netbase-slash-amd64-colon-spdx","relatedSpdxElement":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-netbase-underscore-6.4-underscore-amd64","relationshipType":"GENERATED_FROM"},{"spdxElementId":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-amd64","relatedSpdxElement":"SPDXRef--at-bookworm-slash--slash-netbase-slash-amd64-colon-spdx","relationshipType":"DEPENDS_ON"},{"spdxElementId":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-fat-underscore-image","relatedSpdxElement":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-amd64","relationshipType":"CONTAINS"}]} -------------------------------------------------------------------------------- /private/pkg/test/oci_image/image_amd64.spdx.json: -------------------------------------------------------------------------------- 1 | {"spdxVersion":"SPDX-2.3","dataLicense":"CC0-1.0","SPDXID":"SPDXRef-DOCUMENT","name":"//private/pkg/test/oci_image:image_amd64","documentNamespace":"http://spdx.org/spdxdocs/distroless/-slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-amd64","creationInfo":{"licenseListVersion":"NOASSERTION","creators":["Organization: distroless"],"created":"1970-01-01T00:00:00Z"},"packages":[{"name":"//private/pkg/test/oci_image:image_amd64","SPDXID":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-amd64","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION"},{"name":"netbase","SPDXID":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-netbase-underscore-6.4-underscore-amd64","versionInfo":"6.4","supplier":"Person: Marco d'Itri \\u003cmd@linux.it\\u003e","downloadLocation":"https://snapshot.debian.org/archive/debian/20250517T203043Z/pool/main/n/netbase/netbase_6.4_all.deb","checksums":[{"algorithm":"SHA256","checksumValue":"29b23c48c0fe6f878e56c5ddc9f65d1c05d729360f3690a593a8c795031cd867"}],"copyrightText":"Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nComment:\n This package was created by Peter Tobias tobias@et-inf.fho-emden.de on\n Wed, 24 Aug 1994 21:33:28 +0200 and maintained by Anthony Towns\n \u003cajt@debian.org\u003e until 2001.\n It is currently maintained by Marco d'Itri \u003cmd@linux.it\u003e.\n\nFiles: *\nCopyright:\n Copyright (c) 1994-1998 Peter Tobias\n Copyright (c) 1998-2001 Anthony Towns\n Copyright (c) 2002-2022 Marco d'Itri\nLicense: GPL-2\n This program is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License, version 2, as\n published by the Free Software Foundation.\n .\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNU General Public License for more details.\n .\n You should have received a copy of the GNU General Public License along\n with this program; if not, write to the Free Software Foundation,\n Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n .\n On Debian systems, the complete text of the GNU General Public License\n version 2 can be found in '/usr/share/common-licenses/GPL-2'.\n","summary":"Basic TCP/IP networking system","description":"Basic TCP/IP networking system\nThis package provides the necessary infrastructure for basic TCP/IP based\nnetworking.\n.\nIn particular, it supplies common name-to-number mappings in /etc/services,\n/etc/rpc, /etc/protocols and /etc/ethertypes.","externalRefs":[{"referenceCategory":"PACKAGE-MANAGER","referenceType":"purl","referenceLocator":"pkg:deb/debian/netbase@6.4?arch=all"}]},{"name":"@bookworm//netbase/amd64:spdx","SPDXID":"SPDXRef--at-bookworm-slash--slash-netbase-slash-amd64-colon-spdx","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION","description":"Generated from netbase@6.4"}],"relationships":[{"spdxElementId":"SPDXRef-DOCUMENT","relatedSpdxElement":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-amd64","relationshipType":"DESCRIBES"},{"spdxElementId":"SPDXRef--at-bookworm-slash--slash-netbase-slash-amd64-colon-spdx","relatedSpdxElement":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-netbase-underscore-6.4-underscore-amd64","relationshipType":"GENERATED_FROM"},{"spdxElementId":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-amd64","relatedSpdxElement":"SPDXRef--at-bookworm-slash--slash-netbase-slash-amd64-colon-spdx","relationshipType":"DEPENDS_ON"}]} -------------------------------------------------------------------------------- /private/pkg/test/oci_image/image_arm64.spdx.json: -------------------------------------------------------------------------------- 1 | {"spdxVersion":"SPDX-2.3","dataLicense":"CC0-1.0","SPDXID":"SPDXRef-DOCUMENT","name":"//private/pkg/test/oci_image:image_arm64","documentNamespace":"http://spdx.org/spdxdocs/distroless/-slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-arm64","creationInfo":{"licenseListVersion":"NOASSERTION","creators":["Organization: distroless"],"created":"1970-01-01T00:00:00Z"},"packages":[{"name":"//private/pkg/test/oci_image:image_arm64","SPDXID":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-arm64","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION"},{"name":"base-files","SPDXID":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-base-files-underscore-12.4-p-deb12u11-underscore-arm64","versionInfo":"12.4+deb12u11","supplier":"Person: Santiago Vila \\u003csanvila@debian.org\\u003e","downloadLocation":"https://snapshot.debian.org/archive/debian/20250517T203043Z/pool/main/b/base-files/base-files_12.4+deb12u11_arm64.deb","checksums":[{"algorithm":"SHA256","checksumValue":"d758534164a8b6d0fc30407f81175e70a8035ba75853abd70311a6fcd3294822"}],"copyrightText":"This is the Debian prepackaged version of the Debian Base System\nMiscellaneous files. These files were written by Ian Murdock\n\u003cimurdock@debian.org\u003e and Bruce Perens \u003cbruce@pixar.com\u003e.\n\nThis package was first put together by Bruce Perens \u003cBruce@Pixar.com\u003e,\nfrom his own sources.\n\nThe GNU Public Licenses in /usr/share/common-licenses were taken from\nftp.gnu.org and are copyrighted by the Free Software Foundation, Inc.\n\nThe Artistic License in /usr/share/common-licenses is the one coming\nfrom Perl and its SPDX name is \"Artistic License 1.0 (Perl)\".\n\n\nCopyright (C) 1995-2011 Software in the Public Interest.\n\nThis program is free software; you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation; either version 2 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nOn Debian systems, the complete text of the GNU General\nPublic License can be found in `/usr/share/common-licenses/GPL'.\n","summary":"Debian base system miscellaneous files","description":"Debian base system miscellaneous files\nThis package contains the basic filesystem hierarchy of a Debian system, and\nseveral important miscellaneous files, such as /etc/debian_version,\n/etc/host.conf, /etc/issue, /etc/motd, /etc/profile, and others,\nand the text of several common licenses in use on Debian systems.","externalRefs":[{"referenceCategory":"PACKAGE-MANAGER","referenceType":"purl","referenceLocator":"pkg:deb/debian/base-files@12.4+deb12u11?arch=arm64"}]},{"name":"@bookworm//base-files/arm64:spdx","SPDXID":"SPDXRef--at-bookworm-slash--slash-base-files-slash-arm64-colon-spdx","downloadLocation":"NOASSERTION","copyrightText":"NOASSERTION","description":"Generated from base-files@12.4+deb12u11"}],"relationships":[{"spdxElementId":"SPDXRef-DOCUMENT","relatedSpdxElement":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-arm64","relationshipType":"DESCRIBES"},{"spdxElementId":"SPDXRef--at-bookworm-slash--slash-base-files-slash-arm64-colon-spdx","relatedSpdxElement":"SPDXRef--at-rules-underscore-distroless~~apt~bookworm-underscore-base-files-underscore-12.4-p-deb12u11-underscore-arm64","relationshipType":"GENERATED_FROM"},{"spdxElementId":"SPDXRef--slash--slash-private-slash-pkg-slash-test-slash-oci-underscore-image-colon-image-underscore-arm64","relatedSpdxElement":"SPDXRef--at-bookworm-slash--slash-base-files-slash-arm64-colon-spdx","relationshipType":"DEPENDS_ON"}]} -------------------------------------------------------------------------------- /private/repos/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/distroless/4c40ba78166a7e559bcd41df2083d45bdb4212e7/private/repos/BUILD.bazel -------------------------------------------------------------------------------- /private/repos/deb/BUILD.bazel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/distroless/4c40ba78166a7e559bcd41df2083d45bdb4212e7/private/repos/deb/BUILD.bazel -------------------------------------------------------------------------------- /private/repos/deb/bookworm.yaml: -------------------------------------------------------------------------------- 1 | # debian 12 2 | version: 1 3 | 4 | sources: 5 | - channel: bookworm main 6 | url: https://snapshot.debian.org/archive/debian/20250517T203043Z 7 | - channel: bookworm-updates main 8 | url: https://snapshot.debian.org/archive/debian/20250517T203043Z 9 | - channel: bookworm-security main 10 | url: https://snapshot.debian.org/archive/debian-security/20250517T203043Z 11 | 12 | archs: 13 | - amd64 14 | - arm64 15 | - armhf 16 | - s390x 17 | - ppc64el 18 | 19 | packages: 20 | - base-files 21 | - ca-certificates 22 | - libc6 23 | - libc-bin 24 | - libssl3 25 | - netbase 26 | - openssl 27 | - tzdata 28 | # c++ 29 | - gcc-12-base 30 | - libgcc-s1 31 | - libgomp1 32 | - libstdc++6 33 | -------------------------------------------------------------------------------- /private/repos/deb/bookworm_java.yaml: -------------------------------------------------------------------------------- 1 | # debian 12, limited architectures, java only 2 | version: 1 3 | 4 | sources: 5 | - channel: bookworm main 6 | url: https://snapshot.debian.org/archive/debian/20250517T203043Z 7 | - channel: bookworm-updates main 8 | url: https://snapshot.debian.org/archive/debian/20250517T203043Z 9 | - channel: bookworm-security main 10 | url: https://snapshot.debian.org/archive/debian-security/20250517T203043Z 11 | 12 | archs: 13 | - amd64 14 | - arm64 15 | - s390x 16 | - ppc64el 17 | 18 | packages: 19 | - fontconfig-config 20 | - fonts-dejavu-core 21 | - libbrotli1 22 | - libcrypt1 # TODO: glibc library for -lcrypt; maybe should be in cc? 23 | - libexpat1 24 | - libfontconfig1 25 | - libfreetype6 26 | - libglib2.0-0 27 | - libgraphite2-3 28 | - libharfbuzz0b 29 | - libjpeg62-turbo 30 | - liblcms2-2 31 | - libpcre2-8-0 32 | - libpng16-16 33 | - libuuid1 34 | - openjdk-17-jdk-headless 35 | - openjdk-17-jre-headless 36 | - zlib1g 37 | # java image builds off base (not cc) 38 | - gcc-12-base 39 | - libgcc-s1 40 | - libstdc++6 41 | -------------------------------------------------------------------------------- /private/repos/deb/bookworm_python.yaml: -------------------------------------------------------------------------------- 1 | # debian 12, limited architectures, java only 2 | version: 1 3 | 4 | sources: 5 | - channel: bookworm main 6 | url: https://snapshot.debian.org/archive/debian/20250517T203043Z 7 | - channel: bookworm-updates main 8 | url: https://snapshot.debian.org/archive/debian/20250517T203043Z 9 | - channel: bookworm-security main 10 | url: https://snapshot.debian.org/archive/debian-security/20250517T203043Z 11 | 12 | archs: 13 | - amd64 14 | - arm64 15 | 16 | packages: 17 | - dash 18 | - libbz2-1.0 19 | - libcom-err2 20 | - libcrypt1 # TODO: glibc library for -lcrypt; maybe should be in cc? 21 | - libdb5.3 22 | - libexpat1 23 | - libffi8 24 | - libgssapi-krb5-2 25 | - libk5crypto3 26 | - libkeyutils1 27 | - libkrb5-3 28 | - libkrb5support0 29 | - liblzma5 30 | - libncursesw6 31 | - libnsl2 32 | - libpython3.11-minimal 33 | - libpython3.11-stdlib 34 | - libreadline8 35 | - libsqlite3-0 36 | - libtinfo6 37 | - libtirpc3 38 | - libuuid1 39 | - python3-distutils 40 | - python3.11-minimal 41 | - zlib1g 42 | - libc-bin 43 | -------------------------------------------------------------------------------- /private/repos/deb/deb.MODULE.bazel: -------------------------------------------------------------------------------- 1 | "debian dependencies" 2 | 3 | apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt") 4 | apt.install( 5 | name = "bookworm", 6 | lock = "//private/repos/deb:bookworm.lock.json", 7 | manifest = "//private/repos/deb:bookworm.yaml", 8 | package_template = "//private/repos/deb:package.BUILD.tmpl", 9 | resolve_transitive = False, 10 | ) 11 | apt.install( 12 | name = "bookworm_java", 13 | lock = "//private/repos/deb:bookworm_java.lock.json", 14 | manifest = "//private/repos/deb:bookworm_java.yaml", 15 | package_template = "//private/repos/deb:package.BUILD.tmpl", 16 | resolve_transitive = False, 17 | ) 18 | apt.install( 19 | name = "bookworm_python", 20 | lock = "//private/repos/deb:bookworm_python.lock.json", 21 | manifest = "//private/repos/deb:bookworm_python.yaml", 22 | package_template = "//private/repos/deb:package.BUILD.tmpl", 23 | resolve_transitive = False, 24 | ) 25 | use_repo(apt, "bookworm", "bookworm_java", "bookworm_python") 26 | 27 | ### VERSIONS HUB REPO ### 28 | version = use_extension("//private/extensions:version.bzl", "version") 29 | version.from_lock( 30 | lock = "//private/repos/deb:bookworm_python.lock.json", 31 | repo_name = "bookworm_python", 32 | ) 33 | version.from_lock( 34 | lock = "//private/repos/deb:bookworm_java.lock.json", 35 | repo_name = "bookworm_java", 36 | ) 37 | version.from_lock( 38 | lock = "//private/repos/deb:bookworm.lock.json", 39 | repo_name = "bookworm", 40 | ) 41 | use_repo(version, "versions") 42 | -------------------------------------------------------------------------------- /private/repos/deb/package.BUILD.tmpl: -------------------------------------------------------------------------------- 1 | """Generated by distroless. DO NOT EDIT!""" 2 | 3 | load("@@//private/pkg:debian_spdx.bzl", "debian_spdx") 4 | load("@@//private/util:merge_providers.bzl", "merge_providers") 5 | load("@@rules_distroless~//apt:defs.bzl", "dpkg_statusd") 6 | load("@@rules_pkg~//:pkg.bzl", "pkg_tar") 7 | 8 | alias( 9 | name = "control", 10 | actual = "@{repo_name}//:control", 11 | visibility = ["//visibility:public"] 12 | ) 13 | alias( 14 | name = "data", 15 | actual = "@{repo_name}//:data", 16 | visibility = ["//visibility:public"] 17 | ) 18 | 19 | dpkg_statusd( 20 | name = "statusd", 21 | control = ":control", 22 | package_name = "{name}" 23 | ) 24 | 25 | pkg_tar( 26 | name = "data_statusd", 27 | # workaround for https://github.com/bazelbuild/rules_pkg/issues/652 28 | package_dir = "./", 29 | deps = [ 30 | ":data", 31 | ":statusd" 32 | ] 33 | ) 34 | 35 | debian_spdx( 36 | name = "spdx", 37 | control = ":control", 38 | data = ":data", 39 | package_name = "{name}", 40 | spdx_id = "{repo_name}", 41 | sha256 = "{sha256}", 42 | urls = {urls} 43 | ) 44 | 45 | merge_providers( 46 | name = "{target_name}", 47 | srcs = [":data_statusd", ":spdx"], 48 | visibility = ["//visibility:public"], 49 | ) -------------------------------------------------------------------------------- /private/repos/java.MODULE.bazel: -------------------------------------------------------------------------------- 1 | "repositories for java" 2 | 3 | #VERSION 21.0.7 4 | 5 | java = use_extension("//private/extensions:java.bzl", "java") 6 | java.archive( 7 | name = "temurin21_jre_amd64", 8 | sha256 = "6d48379e00d47e6fdd417e96421e973898ac90765ea8ff2d09ae0af6d5d6a1c6", 9 | strip_prefix = "jdk-21.0.7+6-jre", 10 | urls = ["https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jre_x64_linux_hotspot_21.0.7_6.tar.gz"], 11 | version = "21.0.7+6", 12 | plain_version = "21.0.7", 13 | architecture = "amd64", 14 | ) 15 | java.archive( 16 | name = "temurin21_jdk_amd64", 17 | sha256 = "974d3acef0b7193f541acb61b76e81670890551366625d4f6ca01b91ac152ce0", 18 | strip_prefix = "jdk-21.0.7+6", 19 | urls = ["https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_x64_linux_hotspot_21.0.7_6.tar.gz"], 20 | version = "21.0.7+6", 21 | plain_version = "21.0.7", 22 | architecture = "amd64", 23 | ) 24 | java.archive( 25 | name = "temurin21_jre_arm64", 26 | sha256 = "ab455a401d25e0cd20e652d2ee72e9f56beba0d9faac5a5c62c9b27a19df804b", 27 | strip_prefix = "jdk-21.0.7+6-jre", 28 | urls = ["https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jre_aarch64_linux_hotspot_21.0.7_6.tar.gz"], 29 | version = "21.0.7+6", 30 | plain_version = "21.0.7", 31 | architecture = "arm64", 32 | ) 33 | java.archive( 34 | name = "temurin21_jdk_arm64", 35 | sha256 = "31dba70ba928c78c20d62049ac000f79f7a7ab11f9d9c11e703f52d60aa64f93", 36 | strip_prefix = "jdk-21.0.7+6", 37 | urls = ["https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.7_6.tar.gz"], 38 | version = "21.0.7+6", 39 | plain_version = "21.0.7", 40 | architecture = "arm64", 41 | ) 42 | java.archive( 43 | name = "temurin21_jre_ppc64le", 44 | sha256 = "721d3b374cb333269d487e7f99e2d247576c989d2e08a2842738ef62f432bcbd", 45 | strip_prefix = "jdk-21.0.7+6-jre", 46 | urls = ["https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jre_ppc64le_linux_hotspot_21.0.7_6.tar.gz"], 47 | version = "21.0.7+6", 48 | plain_version = "21.0.7", 49 | architecture = "ppc64le", 50 | ) 51 | java.archive( 52 | name = "temurin21_jdk_ppc64le", 53 | sha256 = "2ddc0dc14b07d9e853875aac7f84c23826fff18b9cea618c93efe0bcc8f419c2", 54 | strip_prefix = "jdk-21.0.7+6", 55 | urls = ["https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.7%2B6/OpenJDK21U-jdk_ppc64le_linux_hotspot_21.0.7_6.tar.gz"], 56 | version = "21.0.7+6", 57 | plain_version = "21.0.7", 58 | architecture = "ppc64le", 59 | ) 60 | use_repo(java, "java_versions", "temurin21_jdk_amd64", "temurin21_jdk_arm64", "temurin21_jdk_ppc64le", "temurin21_jre_amd64", "temurin21_jre_arm64", "temurin21_jre_ppc64le") 61 | -------------------------------------------------------------------------------- /private/stamp.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | # Do not error if the user doesn't have gcloud installed. 5 | if [ -z "${PROJECT_ID:-}" ]; then 6 | if which gcloud > /dev/null; then 7 | PROJECT_ID="$(gcloud config get-value core/project)" 8 | else 9 | # some registries don't allow uppercase chars. we'll use lowercase ones to get meaningful error messages. 10 | PROJECT_ID="no-project" 11 | fi 12 | fi 13 | 14 | echo "PROJECT_ID ${PROJECT_ID}" 15 | echo "COMMIT_SHA ${COMMIT_SHA:-no-commit-sha}" 16 | echo "REGISTRY ${REGISTRY:-gcr.io}" -------------------------------------------------------------------------------- /private/tools/BUILD.bazel: -------------------------------------------------------------------------------- 1 | sh_binary( 2 | name = "diff", 3 | srcs = ["diff.bash"], 4 | args = [ 5 | "--head-ref", 6 | "test", 7 | "--base-ref", 8 | "test", 9 | "--report", 10 | "./report.log", 11 | "--query-bazel", 12 | "--registry-spawn-https", 13 | "--cd-into-workspace", 14 | ], 15 | ) 16 | -------------------------------------------------------------------------------- /private/tools/diff.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o pipefail -o errexit -o nounset 3 | 4 | # ./private/tools/diff.bash --head-ref test --base-ref test --query-bazel --registry-spawn --report ./report.log 5 | 6 | STDERR=$(mktemp) 7 | 8 | # Upon exiting, stop the registry and print STDERR on non-zero exit code. 9 | on_exit() { 10 | last_exit_code=$? 11 | set +o errexit 12 | if [[ $last_exit_code != 0 ]]; then 13 | echo "" 14 | echo "💥 Something went wrong." 15 | if [[ $(wc -c <"${STDERR}") -gt 0 ]]; then 16 | echo "" 17 | echo "Here's the STDERR:" 18 | echo "" 19 | cat $STDERR 20 | fi 21 | fi 22 | pkill -P $$ 23 | } 24 | trap "on_exit" EXIT 25 | 26 | PID= 27 | HEAD_REF= 28 | BASE_REF= 29 | QUERY_FILE= 30 | REPORT_FILE= 31 | REGISTRY= 32 | JOBS= 33 | STDERR=$(mktemp) 34 | CHANGED_IMAGES_FILE=$(mktemp) 35 | SET_GITHUB_OUTPUT="0" 36 | ONLY= 37 | SKIP_INDEX="0" 38 | 39 | while (($# > 0)); do 40 | case $1 in 41 | --base-ref) 42 | BASE_REF="$2" 43 | shift 2 44 | ;; 45 | --head-ref) 46 | HEAD_REF="$2" 47 | shift 2 48 | ;; 49 | --registry) 50 | REGISTRY="$2" 51 | shift 2 52 | ;; 53 | --registry-spawn) 54 | REGISTRY="spawn" 55 | shift 56 | ;; 57 | --registry-spawn-https) 58 | REGISTRY="spawn_https" 59 | shift 60 | ;; 61 | --query) 62 | QUERY_FILE="$2" 63 | shift 2 64 | ;; 65 | --query-bazel) 66 | QUERY_FILE="bazel" 67 | shift 68 | ;; 69 | --report) 70 | REPORT_FILE="$2" 71 | shift 2 72 | ;; 73 | --set-github-output-on-diff) 74 | SET_GITHUB_OUTPUT="1" 75 | echo "changed_targets=" >> "$GITHUB_OUTPUT" 76 | shift 77 | ;; 78 | --jobs) 79 | JOBS="$2" 80 | shift 2 81 | ;; 82 | --logs) 83 | STDERR="$2" 84 | shift 2 85 | ;; 86 | --only) 87 | ONLY="$2" 88 | shift 2 89 | ;; 90 | --cd-into-workspace) 91 | cd $BUILD_WORKSPACE_DIRECTORY 92 | shift 93 | ;; 94 | --skip-image-index) 95 | SKIP_INDEX="1" 96 | shift 97 | ;; 98 | *) 99 | echo "unknown arg $1" 100 | exit 1 101 | ;; 102 | esac 103 | done 104 | 105 | if [[ -z "${REGISTRY}" ]]; then 106 | echo "--registry is required." 107 | exit 1 108 | fi 109 | 110 | if [[ -z "${BASE_REF}" ]]; then 111 | echo "--base-ref is required." 112 | exit 1 113 | fi 114 | 115 | if [[ -z "${HEAD_REF}" ]]; then 116 | echo "--head-ref is required." 117 | exit 1 118 | fi 119 | 120 | if [[ -z "${QUERY_FILE}" ]]; then 121 | echo "--query or --query-bazel must be provided" 122 | exit 1 123 | fi 124 | 125 | # Redirect stderr to the $STDERR temp file for the rest of the script. 126 | exec 2>>"${STDERR}" 127 | 128 | DISK_STORAGE="/tmp/diff-storage" 129 | 130 | if [[ "${QUERY_FILE}" == "bazel" ]]; then 131 | bazel build :sign_and_push.query 132 | QUERY_FILE=$(bazel cquery --output=files :sign_and_push.query) 133 | fi 134 | 135 | if [[ "${REGISTRY}" == "spawn_https" ]]; then 136 | # Make a self signed cert 137 | rm -f /tmp/localhost.pem /tmp/localhost-key.pem 138 | rm -rf $DISK_STORAGE 139 | mkcert -install 140 | (cd /tmp && mkcert localhost) 141 | echo '{ 142 | "http":{ 143 | "address":"127.0.0.1", "port":"4564", 144 | "tls": { 145 | "cert":"/tmp/localhost.pem", 146 | "key":"/tmp/localhost-key.pem" 147 | } 148 | }, 149 | "log": { "level": "info" }, 150 | "storage":{"rootDirectory":"/tmp/diff-storage"} 151 | }' >/tmp/cfg.json 152 | REGISTRY="localhost:4564" 153 | zot serve /tmp/cfg.json 1>&2 & 154 | sleep 1 155 | fi 156 | 157 | if [[ "${REGISTRY}" == "spawn" ]]; then 158 | rm -rf $DISK_STORAGE 159 | mkdir $DISK_STORAGE 160 | REGISTRY="localhost:4564" 161 | crane registry serve --address "$REGISTRY" --disk "$DISK_STORAGE" & 162 | fi 163 | 164 | stamp_stage() { 165 | local str="$1" 166 | str=${str/"{COMMIT_SHA}"/"${HEAD_REF}"} 167 | str=${str/"{REGISTRY}"/"${REGISTRY}"} 168 | echo ${str/"{PROJECT_ID}"/"stage"} 169 | } 170 | 171 | stamp_origin() { 172 | local str=$1 173 | str=${str/"{COMMIT_SHA}"/"${BASE_REF}"} 174 | str=${str/"{REGISTRY}"/"gcr.io"} 175 | echo ${str/"{PROJECT_ID}"/"distroless"} 176 | } 177 | 178 | function test_image() { 179 | IFS=" " read -r repo image_label <<<"$1" 180 | 181 | if [[ "${ONLY}" != "" && "${ONLY}" != "$image_label" ]]; then 182 | return 183 | fi 184 | 185 | repo_origin=$(stamp_origin "$repo") 186 | repo_stage=$(stamp_stage "$repo") 187 | 188 | if [[ "${SKIP_INDEX}" == "1" ]]; then 189 | if ! crane manifest "$repo_origin" | jq -e '.mediaType == "application/vnd.oci.image.manifest.v1+json"' > /dev/null; then 190 | echo "⏭️ Skipping image index $repo_origin" 191 | return 192 | fi 193 | fi 194 | 195 | echo "" 196 | echo "🚧 Diffing $repo_origin against $repo_stage" 197 | echo "" 198 | 199 | bazel build "$image_label" 200 | crane push "$(bazel cquery --output=files $image_label)" "$repo_stage" 201 | if ! diffoci diff --pull=always --all-platforms --semantic "$repo_origin" "$repo_stage"; then 202 | echo "" 203 | echo " 🔬 To reproduce: bazel run //private/tools:diff -- --only $image_label" 204 | echo "" 205 | echo "👎 $repo_origin and $repo_stage are different." 206 | if [[ "${SET_GITHUB_OUTPUT}" == "1" ]]; then 207 | echo "$image_label" >> "$CHANGED_IMAGES_FILE" 208 | fi 209 | else 210 | echo "" 211 | echo "👍 $repo_origin and $repo_stage are identical." 212 | fi 213 | } 214 | 215 | if [[ -n "${REPORT_FILE}" ]]; then 216 | echo "Report can be found in: $REPORT_FILE" 217 | echo -n "" >$REPORT_FILE 218 | sleep 1 219 | # Redirect rest of the file into both report file and stdout 220 | exec 1> >(tee -a "${REPORT_FILE}") 221 | fi 222 | 223 | # Parallelize using gnu parallel 224 | if [[ "${JOBS}" -gt 0 ]]; then 225 | export HEAD_REF BASE_REF REGISTRY REPORT_FILE SET_GITHUB_OUTPUT ONLY CHANGED_IMAGES_FILE SKIP_INDEX 226 | export -f stamp_origin stamp_stage test_image 227 | cat "${QUERY_FILE}" | parallel --eta --progress --jobs "${JOBS}" "set -o pipefail -o errexit -o nounset && test_image" 228 | else 229 | while IFS= read -r line || [ -n "$line" ]; do 230 | test_image "${line}" 231 | done <"${QUERY_FILE}" 232 | fi 233 | 234 | if [[ "${SET_GITHUB_OUTPUT}" == "1" ]]; then 235 | echo "changed_targets<> "$GITHUB_OUTPUT" 236 | cat "$CHANGED_IMAGES_FILE" >> "$GITHUB_OUTPUT" 237 | echo "EOF" >> "$GITHUB_OUTPUT" 238 | fi 239 | -------------------------------------------------------------------------------- /private/util/BUILD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleContainerTools/distroless/4c40ba78166a7e559bcd41df2083d45bdb4212e7/private/util/BUILD -------------------------------------------------------------------------------- /private/util/deb.bzl: -------------------------------------------------------------------------------- 1 | "utility functions for constructing debian package labels" 2 | 3 | load("@versions//:versions.bzl", "version") 4 | 5 | DIST_ALIAS = dict( 6 | # bullseye (deprecated) 7 | debian11 = "bullseye", 8 | bullseye = "debian11", 9 | # bookworm 10 | debian12 = "bookworm", 11 | bookworm = "debian12", 12 | ) 13 | 14 | ARCH_ALIAS = dict( 15 | arm = "armhf", 16 | ppc64le = "ppc64el", 17 | arm64 = "arm64", 18 | amd64 = "amd64", 19 | s390x = "s390x", 20 | ) 21 | 22 | def _get_dist_arch_alias(arch, dist): 23 | dist = DIST_ALIAS[dist] 24 | arch = ARCH_ALIAS[arch] 25 | 26 | rel = native.package_name() 27 | if rel == "java": 28 | dist += "_java" 29 | elif rel == "experimental/python3" or rel == "python3": 30 | dist += "_python" 31 | 32 | return (arch, dist) 33 | 34 | def _package(arch, dist, package): 35 | (arch, dist) = _get_dist_arch_alias(arch, dist) 36 | return "@{dist}//{package}/{arch}".format(arch = arch, dist = dist, package = package) 37 | 38 | def _data(arch, dist, package): 39 | (arch, dist) = _get_dist_arch_alias(arch, dist) 40 | return "@{dist}//{package}/{arch}:data".format(arch = arch, dist = dist, package = package) 41 | 42 | def _version(arch, dist, package): 43 | (arch, dist) = _get_dist_arch_alias(arch, dist) 44 | return version(dist, arch, package).raw 45 | 46 | deb = struct( 47 | package = _package, 48 | data = _data, 49 | version = _version, 50 | ) 51 | -------------------------------------------------------------------------------- /private/util/extract.bzl: -------------------------------------------------------------------------------- 1 | "a utility that extracts files from tar" 2 | 3 | load("@aspect_bazel_lib//lib:tar.bzl", "tar_lib") 4 | 5 | def _tar_extract_file_impl(ctx): 6 | bsdtar = ctx.toolchains[tar_lib.toolchain_type] 7 | parts = ctx.attr.file.split("/") 8 | output = ctx.actions.declare_file("/".join([ctx.label.name, parts[-1]])) 9 | 10 | args = ctx.actions.args() 11 | args.add("--extract") 12 | args.add("-C", "/".join([ctx.bin_dir.path, ctx.label.package, ctx.label.name])) 13 | args.add("--file", ctx.file.archive) 14 | args.add("--include", ctx.attr.file) 15 | args.add("--strip-components={}".format(len(parts) - 1)) 16 | 17 | ctx.actions.run( 18 | executable = bsdtar.tarinfo.binary, 19 | inputs = [ctx.file.archive], 20 | outputs = [output], 21 | tools = bsdtar.default.files, 22 | arguments = [args], 23 | ) 24 | 25 | return [ 26 | DefaultInfo(files = depset([output])), 27 | ] 28 | 29 | tar_extract_file = rule( 30 | implementation = _tar_extract_file_impl, 31 | attrs = { 32 | "archive": attr.label(allow_single_file = True, mandatory = True), 33 | "file": attr.string(mandatory = True), 34 | }, 35 | toolchains = [tar_lib.toolchain_type], 36 | ) 37 | -------------------------------------------------------------------------------- /private/util/java_cacerts.bzl: -------------------------------------------------------------------------------- 1 | "java ca certificates" 2 | 3 | load("@rules_distroless//distroless:defs.bzl", "java_keystore") 4 | load("//common:variables.bzl", "MTIME") 5 | load(":extract.bzl", "tar_extract_file") 6 | 7 | def java_cacerts(name, archive): 8 | tar_extract_file( 9 | name = name + "_extract", 10 | archive = archive, 11 | file = "./etc/ssl/certs/ca-certificates.crt", 12 | ) 13 | 14 | java_keystore( 15 | name = name, 16 | certificates = [ 17 | ":" + name + "_extract", 18 | ], 19 | time = MTIME, 20 | ) 21 | -------------------------------------------------------------------------------- /private/util/merge_providers.bzl: -------------------------------------------------------------------------------- 1 | "a utility rule that merges DefaultInfo and OutputGroupInfo providers" 2 | 3 | SKIP = "_hidden_top_level_INTERNAL_" 4 | 5 | def _impl(ctx): 6 | output_group_info = {} 7 | default_info = {"files": depset(), "runfiles": ctx.runfiles()} 8 | 9 | for src in ctx.attr.srcs: 10 | if DefaultInfo in src: 11 | default_info = { 12 | "files": depset(transitive = [default_info["files"], src[DefaultInfo].files]), 13 | "runfiles": default_info["runfiles"].merge(src[DefaultInfo].default_runfiles), 14 | } 15 | if OutputGroupInfo in src: 16 | for key in src[OutputGroupInfo]: 17 | if key == SKIP: 18 | continue 19 | depsets = [src[OutputGroupInfo][key]] 20 | if key in output_group_info: 21 | depsets.append(output_group_info[key]) 22 | output_group_info[key] = depset(transitive = depsets) 23 | 24 | return [ 25 | OutputGroupInfo(**output_group_info), 26 | DefaultInfo(**default_info), 27 | ] 28 | 29 | merge_providers = rule( 30 | implementation = _impl, 31 | attrs = { 32 | "srcs": attr.label_list(), 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /python3/BUILD: -------------------------------------------------------------------------------- 1 | load("@container_structure_test//:defs.bzl", "container_structure_test") 2 | load("@rules_oci//oci:defs.bzl", "oci_image", "oci_image_index") 3 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 4 | load("//:checksums.bzl", ARCHITECTURES = "BASE_ARCHITECTURES") 5 | load("//private/util:deb.bzl", "deb") 6 | 7 | package(default_visibility = ["//visibility:public"]) 8 | 9 | USERS = [ 10 | "root", 11 | "nonroot", 12 | ] 13 | 14 | DISTROS = [ 15 | "debian12", 16 | ] 17 | 18 | DISTRO_VERSION = { 19 | "debian12": "3.11", 20 | } 21 | 22 | [ 23 | pkg_tar( 24 | name = "python_aliases_%s" % distro, 25 | symlinks = { 26 | "/usr/bin/python": "/usr/bin/python" + DISTRO_VERSION[distro], 27 | "/usr/bin/python3": "/usr/bin/python" + DISTRO_VERSION[distro], 28 | }, 29 | ) 30 | for distro in DISTROS 31 | ] 32 | 33 | [ 34 | oci_image_index( 35 | name = ("python3" if (not mode) else mode[1:]) + "_" + user + "_" + distro, 36 | images = [ 37 | ("python3" if (not mode) else mode[1:]) + "_" + user + "_" + arch + "_" + distro 38 | for arch in ARCHITECTURES 39 | ], 40 | ) 41 | for mode in [ 42 | "", 43 | ":debug", 44 | ] 45 | for user in USERS 46 | for distro in DISTROS 47 | ] 48 | 49 | [ 50 | oci_image( 51 | name = ("python3" if (not mode) else mode[1:]) + "_" + user + "_" + arch + "_" + distro, 52 | # Based on //cc so that C extensions work properly. 53 | base = "//cc" + (mode if mode else ":cc") + "_" + user + "_" + arch + "_" + distro, 54 | entrypoint = [ 55 | "/usr/bin/python" + DISTRO_VERSION[distro], 56 | ], 57 | # Use UTF-8 encoding for file system: match modern Linux 58 | env = {"LANG": "C.UTF-8"}, 59 | tars = [ 60 | deb.package(arch, distro, "libbz2-1.0"), 61 | deb.package(arch, distro, "libdb5.3"), 62 | deb.package(arch, distro, "libexpat1"), 63 | deb.package(arch, distro, "liblzma5"), 64 | deb.package(arch, distro, "libsqlite3-0"), 65 | deb.package(arch, distro, "libuuid1"), 66 | deb.package(arch, distro, "libncursesw6"), 67 | deb.package(arch, distro, "libtinfo6"), 68 | deb.package(arch, distro, "python3-distutils"), 69 | deb.package(arch, distro, "zlib1g"), 70 | deb.package(arch, distro, "libcom-err2"), 71 | deb.package(arch, distro, "libcrypt1"), 72 | deb.package(arch, distro, "libgssapi-krb5-2"), 73 | deb.package(arch, distro, "libk5crypto3"), 74 | deb.package(arch, distro, "libkeyutils1"), 75 | deb.package(arch, distro, "libkrb5-3"), 76 | deb.package(arch, distro, "libkrb5support0"), 77 | deb.package(arch, distro, "libnsl2"), 78 | deb.package(arch, distro, "libreadline8"), 79 | deb.package(arch, distro, "libtirpc3"), 80 | deb.package(arch, distro, "libffi8"), 81 | deb.package(arch, distro, "libpython3.11-minimal"), 82 | deb.package(arch, distro, "libpython3.11-stdlib"), 83 | deb.package(arch, distro, "python3.11-minimal"), 84 | ":python_aliases_%s" % distro, 85 | ], 86 | ) 87 | for mode in [ 88 | "", 89 | ":debug", 90 | ] 91 | for user in USERS 92 | for arch in ARCHITECTURES 93 | for distro in DISTROS 94 | ] 95 | 96 | [ 97 | container_structure_test( 98 | name = "python3_" + user + "_" + arch + "_" + distro + "_test", 99 | size = "medium", 100 | configs = ["testdata/python3.yaml"], 101 | image = ":python3_" + user + "_" + arch + "_" + distro, 102 | tags = [ 103 | "manual", 104 | arch, 105 | ], 106 | ) 107 | for user in USERS 108 | for arch in ARCHITECTURES 109 | for distro in DISTROS 110 | ] 111 | 112 | # tests for version-specific things 113 | [ 114 | container_structure_test( 115 | name = "version_specific_" + user + "_" + arch + "_" + distro + "_test", 116 | size = "medium", 117 | configs = ["testdata/" + distro + ".yaml"], 118 | image = ":python3_" + user + "_" + arch + "_" + distro, 119 | tags = [ 120 | "manual", 121 | arch, 122 | ], 123 | ) 124 | for user in USERS 125 | for arch in ARCHITECTURES 126 | for distro in DISTROS 127 | ] 128 | -------------------------------------------------------------------------------- /python3/README.md: -------------------------------------------------------------------------------- 1 | # Documentation for `gcr.io/distroless/python3` 2 | 3 | ## Image Contents 4 | 5 | This image contains a minimal Linux, Python-based runtime. 6 | 7 | Specifically, the image contains everything in the [base image](../base/README.md), plus: 8 | 9 | * Python 3 and its dependencies. 10 | * No shell and no support for ctypes 11 | 12 | ## Usage 13 | 14 | The entrypoint of this image is set to "python", so this image expects users to supply a path to a .py file in the CMD. 15 | 16 | See the Python [Hello World](../examples/python3/) directory for an example. 17 | -------------------------------------------------------------------------------- /python3/testdata/debian12.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: version 4 | command: ["/usr/bin/python3", "--version"] 5 | expectedOutput: ["Python 3.11.2"] 6 | - name: symlink 7 | command: ["/usr/bin/python", "--version"] 8 | expectedOutput: ["Python 3.11.2"] 9 | -------------------------------------------------------------------------------- /python3/testdata/python3.yaml: -------------------------------------------------------------------------------- 1 | schemaVersion: "1.0.0" 2 | commandTests: 3 | - name: hello 4 | command: ["/usr/bin/python3", "-c", "print('Hello World')"] 5 | expectedOutput: ['Hello World'] 6 | 7 | # ensure there is no shell 8 | - name: no_shell 9 | command: ["/usr/bin/python3", "-c", 10 | "import subprocess, sys; subprocess.check_call(sys.executable + ' -h', shell=True)"] 11 | exitCode: 1 12 | 13 | # debian's default python3 includes a partial version of distutils causing virtualenv to fail 14 | # ensure we have the full version so virtualenvs work with distroless 15 | - name: distutils_works 16 | command: ["/usr/bin/python3", "-c", "import distutils.dist"] 17 | exitCode: 0 18 | 19 | # file names are UTF-8: default for modern Linux systems 20 | # The \xe9 backslash must be double-escaped to avoid YAML string parsing weirdness 21 | - name: filesystem_utf8 22 | command: ["/usr/bin/python3", "-c", "open(u'h\\xe9llo', 'w'); import sys; print(sys.getfilesystemencoding())"] 23 | expectedOutput: ['utf-8'] 24 | 25 | # the print function should output UTF-8 26 | - name: print_utf8 27 | command: ["/usr/bin/python3", "-c", "print(u'h\\xe9llo.txt')"] 28 | expectedOutput: ['h\xe9llo'] 29 | 30 | # import every module installed with the Python package 31 | - name: import_everything 32 | exitCode: 0 33 | expectedOutput: ['FINISHED ENTIRE SCRIPT'] 34 | command: 35 | - "/usr/bin/python3" 36 | - "-c" 37 | # multi-line YAML string with Python script that imports all modules that are installed. 38 | # This ensures we have the right native library dependencies. 39 | - | 40 | import pkgutil 41 | 42 | skip_modules = frozenset(( 43 | # Windows-specific modules 44 | 'asyncio.windows_events', 45 | 'asyncio.windows_utils', 46 | 'ctypes.wintypes', 47 | 'distutils._msvccompiler', 48 | 'distutils.command.bdist_msi', 49 | 'distutils.msvc9compiler', 50 | 'encodings.cp65001', 51 | 'encodings.mbcs', 52 | 'encodings.oem', 53 | 'multiprocessing.popen_spawn_win32', 54 | 'winreg', 55 | 56 | # Python regression tests "for internal use by Python only" 57 | 'test', 58 | 59 | # calls sys.exit 60 | 'unittest.__main__', 61 | 'venv.__main__', 62 | 63 | # depends on things not installed by default on Debian 64 | 'dbm.gnu', 65 | 'lib2to3.pgen2.conv', 66 | 'turtle', 67 | )) 68 | 69 | # pass an error handler so the test fails if there are broken standard library packages 70 | def walk_packages_onerror(failed_module_name): 71 | raise Exception('failed to import module: {}'.format(repr(failed_module_name))) 72 | for module_info in pkgutil.walk_packages(onerror=walk_packages_onerror): 73 | module_name = module_info.name 74 | if module_name in skip_modules or module_name.startswith('test.'): 75 | continue 76 | 77 | __import__(module_name) 78 | print('imported {}'.format(module_name)) 79 | 80 | # ensures some module does not exit early (e.g unittest.__main__) 81 | print('FINISHED ENTIRE SCRIPT') 82 | -------------------------------------------------------------------------------- /scripts/update_java_archives.sh: -------------------------------------------------------------------------------- 1 | set -o pipefail -o errexit -o nounset 2 | 3 | # Copyright 2024 Google Inc. All rights reserved. 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # a collection of functions to use when updating java archives from the knife utility 18 | 19 | function get_java_version() { 20 | grep "#VERSION " ./private/repos/java.MODULE.bazel | cut -d" " -f2 21 | } 22 | 23 | function underscore_encode() { 24 | echo "${1/\+/_}" 25 | } 26 | 27 | function generate_java_archives() { 28 | local releases latest_release release_name version plain_version archs archs_deb variants 29 | 30 | releases=$(curl -sSL https://api.github.com/repos/adoptium/temurin21-binaries/releases) 31 | latest_release=$(echo "$releases" | jq -r 'map(select(.name | test("jdk-([0-9.]+)(\\+([0-9]+))?"))) | sort_by(.published_at) | last') 32 | release_name=$(echo "$latest_release" | jq -r '.name') 33 | version=${release_name#jdk-} 34 | plain_version=$([[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]] && echo "${BASH_REMATCH[0]}") 35 | archs=("x64" "aarch64" "ppc64le") 36 | archs_deb=("amd64" "arm64" "ppc64le") 37 | variants=("jre" "jdk") 38 | 39 | cat << EOM 40 | "repositories for java" 41 | 42 | #VERSION ${plain_version} 43 | 44 | java = use_extension("//private/extensions:java.bzl", "java") 45 | EOM 46 | 47 | for arch_index in "${!archs[@]}"; do 48 | for variant in "${variants[@]}"; do 49 | local arch arch_deb name archive_url sha256_name sha256_url sha256 strip_prefix_suffix 50 | 51 | arch=${archs[arch_index]} 52 | arch_deb=${archs_deb[arch_index]} 53 | name="OpenJDK21U-${variant}_${arch}_linux_hotspot_$(underscore_encode "${version}").tar.gz" 54 | archive_url=$(echo "$latest_release" | jq -r --arg NAME "$name" '.assets | .[] | select(.name==$NAME) | .browser_download_url') 55 | [ "$archive_url" ] || { echo "no url found for ${name}"; exit 1; } 56 | sha256_name="${name}.sha256.txt" 57 | sha256_url=$(echo "$latest_release" | jq -r --arg NAME "$sha256_name" '.assets | .[] | select(.name==$NAME) | .browser_download_url') 58 | [ "$sha256_url" ] || { echo "no url found for ${sha256_name}"; exit 1; } 59 | sha256=$(curl -sSL "$sha256_url" | cut -d' ' -f1) 60 | [ "$sha256" ] || { echo "no sha256 downloaded for ${name}"; exit 1; } 61 | 62 | strip_prefix_suffix="-jre" 63 | if [[ ${variant} == "jdk" ]]; then 64 | strip_prefix_suffix="" 65 | fi 66 | 67 | cat << EOM 68 | java.archive( 69 | name = "temurin21_${variant}_${arch_deb}", 70 | sha256 = "${sha256}", 71 | strip_prefix = "${release_name}${strip_prefix_suffix}", 72 | urls = ["${archive_url}"], 73 | version = "${version}", 74 | plain_version = "${plain_version}", 75 | architecture = "${arch_deb}", 76 | ) 77 | EOM 78 | 79 | done 80 | done 81 | 82 | cat << EOM 83 | use_repo(java, "java_versions", "temurin21_jdk_amd64", "temurin21_jdk_arm64", "temurin21_jdk_ppc64le", "temurin21_jre_amd64", "temurin21_jre_arm64", "temurin21_jre_ppc64le") 84 | EOM 85 | } 86 | 87 | function update_test_versions_java21() { 88 | [ "$1" ] || { echo "no old version set in param 1"; exit 1; } 89 | [ "$2" ] || { echo "no new version set in param 2"; exit 1; } 90 | old_version=$1 91 | new_version=$2 92 | sed -i -e "s/$old_version/$new_version/g" java/testdata/java21_* 93 | } 94 | -------------------------------------------------------------------------------- /scripts/update_node_archives.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const crypto = require("crypto"); 3 | const https = require("https"); 4 | const fs = require("fs"); 5 | 6 | if (process.argv.length < 3) { 7 | console.error("Usage: node nodeChecksum.js "); 8 | process.exit(1); 9 | } 10 | 11 | const versions = process.argv[2].split(","); 12 | const architectures = ["amd64", "arm64", "arm", "ppc64le", "s390x"]; 13 | 14 | const nodeVersions = {}; 15 | 16 | const calculateChecksum = (url) => { 17 | return new Promise((resolve, reject) => { 18 | https 19 | .get(url, (res) => { 20 | const hash = crypto.createHash("sha256"); 21 | res.on("data", (data) => { 22 | hash.update(data); 23 | }); 24 | res.on("end", () => { 25 | resolve(hash.digest("hex")); 26 | }); 27 | }) 28 | .on("error", (err) => { 29 | reject(`Error downloading file: ${err.message}`); 30 | }); 31 | }); 32 | }; 33 | 34 | const fetchChecksums = async () => { 35 | for (const nodeVersion of versions) { 36 | const major = parseInt(nodeVersion.split(".")[0]); 37 | nodeVersions[nodeVersion] = {}; 38 | await Promise.all( 39 | architectures.map(async (key) => { 40 | let arch = key; 41 | if (major > 22 && key === "arm") { 42 | return; 43 | } 44 | if (key === "amd64") { 45 | arch = "x64"; 46 | } else if (key === "arm") { 47 | arch = "armv7l"; 48 | } 49 | const url = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-${arch}.tar.gz`; 50 | try { 51 | const checksum = await calculateChecksum(url); 52 | nodeVersions[nodeVersion][key] = { 53 | checksum, 54 | suffix: arch, 55 | }; 56 | } catch (error) { 57 | console.error(error); 58 | } 59 | }), 60 | ); 61 | } 62 | }; 63 | 64 | fetchChecksums().then(() => { 65 | let nodeArchives = `"node" 66 | 67 | BUILD_TMPL = """\\ 68 | # GENERATED BY node_archive.bzl 69 | load("@distroless//private/pkg:debian_spdx.bzl", "debian_spdx") 70 | load("@distroless//private/util:merge_providers.bzl", "merge_providers") 71 | load("@rules_pkg//:pkg.bzl", "pkg_tar") 72 | 73 | pkg_tar( 74 | name = "data", 75 | srcs = glob( 76 | [ 77 | "output/bin/node", 78 | "output/LICENSE", 79 | ], 80 | ), 81 | package_dir = "/nodejs", 82 | strip_prefix = "external/{canonical_name}/output" 83 | ) 84 | 85 | pkg_tar( 86 | name = "_control", 87 | srcs = ["control"] 88 | ) 89 | 90 | debian_spdx( 91 | name = "spdx", 92 | control = ":_control.tar", 93 | data = ":data.tar", 94 | package_name = "{package_name}", 95 | spdx_id = "{spdx_id}", 96 | sha256 = "{sha256}", 97 | urls = [{urls}] 98 | ) 99 | 100 | merge_providers( 101 | name = "{name}", 102 | srcs = [":data", ":spdx"], 103 | visibility = ["//visibility:public"], 104 | ) 105 | """ 106 | 107 | def _impl(rctx): 108 | rctx.report_progress("Fetching {}".format(rctx.attr.package_name)) 109 | rctx.download_and_extract( 110 | url = rctx.attr.urls, 111 | sha256 = rctx.attr.sha256, 112 | type = rctx.attr.type, 113 | stripPrefix = rctx.attr.strip_prefix, 114 | output = "output", 115 | ) 116 | rctx.template( 117 | "control", 118 | rctx.attr.control, 119 | substitutions = { 120 | "{{VERSION}}": rctx.attr.version, 121 | "{{ARCHITECTURE}}": rctx.attr.architecture, 122 | "{{SHA256}}": rctx.attr.sha256, 123 | }, 124 | ) 125 | rctx.file( 126 | "BUILD.bazel", 127 | content = BUILD_TMPL.format( 128 | canonical_name = rctx.attr.name, 129 | name = rctx.attr.name.split("~")[-1], 130 | package_name = rctx.attr.package_name, 131 | spdx_id = rctx.attr.name, 132 | urls = ",".join(['"%s"' % url for url in rctx.attr.urls]), 133 | sha256 = rctx.attr.sha256, 134 | ), 135 | ) 136 | 137 | node_archive = repository_rule( 138 | implementation = _impl, 139 | attrs = { 140 | "urls": attr.string_list(mandatory = True), 141 | "sha256": attr.string(mandatory = True), 142 | "type": attr.string(default = ".tar.gz"), 143 | "strip_prefix": attr.string(), 144 | "package_name": attr.string(default = "nodejs"), 145 | "version": attr.string(mandatory = True), 146 | "architecture": attr.string(mandatory = True), 147 | # control is only used to populate the sbom, see https://github.com/GoogleContainerTools/distroless/issues/1373 148 | # for why writing debian control files to the image is incompatible with scanners. 149 | "control": attr.label(), 150 | }, 151 | ) 152 | 153 | def _node_impl(module_ctx): 154 | mod = module_ctx.modules[0] 155 | 156 | if len(module_ctx.modules) > 1: 157 | fail("node.archive should be called only once") 158 | if not mod.is_root: 159 | fail("node.archive should be called from root module only.") 160 | 161 | # Node (https://nodejs.org/en/about/releases/) 162 | # Follow Node's maintainence schedule and support all LTS versions that are not end of life`; 163 | 164 | for (const nodeVersion of versions) { 165 | const major = parseInt(nodeVersion.split(".")[0]); 166 | 167 | for (const key of architectures) { 168 | if (major > 22 && key === "arm") { 169 | continue; 170 | } 171 | const arch = nodeVersions[nodeVersion][key]; 172 | const url = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-${arch.suffix}.tar.gz`; 173 | 174 | nodeArchives += "\n" 175 | nodeArchives += ` 176 | node_archive( 177 | name = "nodejs${major}_${key}", 178 | sha256 = "${arch.checksum}", 179 | strip_prefix = "node-v${nodeVersion}-linux-${arch.suffix}/", 180 | urls = ["${url}"], 181 | version = "${nodeVersion}", 182 | architecture = "${key}", 183 | control = "//nodejs:control", 184 | )`; 185 | } 186 | 187 | const testData = `schemaVersion: "2.0.0" 188 | commandTests: 189 | - name: nodejs 190 | command: "/nodejs/bin/node" 191 | args: ["--version"] 192 | expectedOutput: ["v${nodeVersion}"] 193 | ` 194 | 195 | fs.writeFile(`nodejs/testdata/nodejs${major}.yaml`, testData, (err) => { 196 | if (err) { 197 | console.error(err); 198 | }} 199 | ); 200 | } 201 | nodeArchives += ` 202 | 203 | return module_ctx.extension_metadata( 204 | root_module_direct_deps = [` 205 | 206 | for (const nodeVersion of versions) { 207 | const major = parseInt(nodeVersion.split(".")[0]); 208 | for (const arch of architectures) { 209 | if (major > 22 && arch === "arm") { 210 | continue; 211 | } 212 | nodeArchives += ` 213 | "nodejs${major}_${arch}",` 214 | } 215 | } 216 | 217 | nodeArchives += ` 218 | ], 219 | root_module_direct_dev_deps = [], 220 | ) 221 | 222 | _archive = tag_class(attrs = {}) 223 | 224 | node = module_extension( 225 | implementation = _node_impl, 226 | tag_classes = { 227 | "archive": _archive, 228 | }, 229 | ) 230 | ` 231 | 232 | // Write output to node_archives.bzl file 233 | fs.writeFile("private/extensions/node.bzl", nodeArchives, (err) => { 234 | if (err) { 235 | console.error(err); 236 | } 237 | }); 238 | }); 239 | --------------------------------------------------------------------------------