From 697cddfe0adb1964f469e272d843b76346c1884a Mon Sep 17 00:00:00 2001 From: Aaron Siddhartha Mondal Date: Tue, 5 Dec 2023 17:31:12 +0100 Subject: [PATCH] Publish container images (#443) Introduce signed, fully reproducible container images that are created on pushes to main and published via GitHub packages. --- .github/workflows/image.yaml | 44 ++++++++++++++++++++++++++++++++++++ .gitignore | 1 + README.md | 41 ++++++++++++++++++++++++++++++++- flake.nix | 37 ++++++++++++++++++++++++++++-- tools/local-image-test.nix | 24 ++++++++++++++++++++ tools/publish-ghcr.nix | 44 ++++++++++++++++++++++++++++++++++++ 6 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/image.yaml create mode 100644 tools/local-image-test.nix create mode 100644 tools/publish-ghcr.nix diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml new file mode 100644 index 000000000..e8943d823 --- /dev/null +++ b/.github/workflows/image.yaml @@ -0,0 +1,44 @@ +--- +name: Create OCI image +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: read-all + +jobs: + publish-image: + runs-on: ubuntu-22.04 + permissions: + packages: write + id-token: write + steps: + - name: Checkout + uses: >- # v3.5.3 + actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 + + - name: Install Nix + uses: >- # v7 + DeterminateSystems/nix-installer-action@5620eb4af6b562c53e4d4628c0b6e4f9d9ae8612 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Cache Nix derivations + uses: >- # Custom commit, last pinned at 2023-11-17. + DeterminateSystems/magic-nix-cache-action@a04e6275a6bea232cd04fc6f3cbf20d4cb02a3e1 + - name: Test image + run: | + nix run .#local-image-test + + - name: Upload image + run: | + nix run .#publish-ghcr + env: + GHCR_REGISTRY: ghcr.io + GHCR_USERNAME: ${{ github.actor }} + GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + GHCR_IMAGE_NAME: ${{ github.repository }} + if: github.ref == 'refs/heads/main' diff --git a/.gitignore b/.gitignore index 01098ff6f..3f90cbc3d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ __pycache__ .direnv/ .DS_Store .pre-commit-config.yaml +result diff --git a/README.md b/README.md index 4eeb1ae44..05048f1ca 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ remote executor for systems that communicate using the [Remote execution protocol](https://github.com/bazelbuild/remote-apis/blob/main/build/bazel/remote/execution/v2/remote_execution.proto) such as [Bazel](https://bazel.build), [Buck2](https://buck2.build), [Goma](https://chromium.googlesource.com/infra/goma/client/) and [Reclient](https://github.com/bazelbuild/reclient). -Supports Unix based operating systems and Windows. +Supports Unix-based operating systems and Windows. ## ❄️ Installing with Nix @@ -30,6 +30,45 @@ For use in production pin the executable to a specific revision: nix run github:TraceMachina/native-link/ ./basic_cas.json ``` +## 📦 Using the OCI image + +See the published [OCI images](https://github.com/TraceMachina/native-link/pkgs/container/native-link) +for pull commands. + +Images are tagged by nix derivation hash. The most recently pushed image +corresponds to the `main` branch. Images are signed by the GitHub action that +produced the image. Note that the [OCI workflow](https://github.com/TraceMachina/native-link/actions/workflows/image.yaml) +might take a few minutes to publish the latest image. + +```sh +# Get the tag for the latest commit +export LATEST=$(nix eval github:TraceMachina/native-link#image.imageTag --raw) + +# Verify the signature +cosign verify ghcr.io/TraceMachina/native-link:${LATEST} \ + --certificate-identity=https://github.com/TraceMachina/native-link/.github/workflows/image.yaml@refs/heads/main \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com +``` + +For use in production pin the image to a specific revision: + +```sh +# Get the tag for a specific commit +export PINNED_TAG=$(nix eval github:TraceMachina/native-link/#image.imageTag --raw) + +# Verify the signature +cosign verify ghcr.io/TraceMachina/native-link:${PINNED_TAG} \ + --certificate-identity=https://github.com/TraceMachina/native-link/.github/workflows/image.yaml@refs/heads/main \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com +``` + +> [!TIP] +> The images are reproducible on `X86_64-unknown-linux-gnu`. If you're on such a +> system you can produce a binary-identical image by building the `.#image` +> flake output locally. Make sure that your `git status` is completely clean and +> aligned with the commit you want to reproduce. Otherwise the image will be +> tainted with a `"dirty"` revision label. + ## 🌱 Building with Bazel **Build requirements:** diff --git a/flake.nix b/flake.nix index 98e224902..cb76bd9d8 100644 --- a/flake.nix +++ b/flake.nix @@ -13,11 +13,11 @@ }; }; - outputs = inputs @ { flake-parts, crane, ... }: + outputs = inputs @ { self, flake-parts, crane, ... }: flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "x86_64-linux" ]; imports = [ inputs.pre-commit-hooks.flakeModule ]; - perSystem = { config, self', inputs', pkgs, system, ... }: + perSystem = { config, pkgs, system, ... }: let customStdenv = import ./tools/llvmStdenv.nix { inherit pkgs; }; @@ -47,6 +47,10 @@ }); hooks = import ./tools/pre-commit-hooks.nix { inherit pkgs; }; + + publish-ghcr = import ./tools/publish-ghcr.nix { inherit pkgs; }; + + local-image-test = import ./tools/local-image-test.nix { inherit pkgs; }; in { apps = { @@ -55,6 +59,29 @@ program = "${native-link}/bin/cas"; }; }; + packages = { + inherit publish-ghcr local-image-test; + default = native-link; + image = pkgs.dockerTools.streamLayeredImage { + name = "native-link"; + contents = [ + native-link + pkgs.dockerTools.caCertificates + ]; + config = { + Entrypoint = [ "/bin/cas" ]; + Labels = { + "org.opencontainers.image.description" = "An RBE compatible, high-performance cache and remote executor."; + "org.opencontainers.image.documentation" = "https://github.com/TraceMachina/native-link"; + "org.opencontainers.image.licenses" = "Apache-2.0"; + "org.opencontainers.image.revision" = "${self.rev or self.dirtyRev or "dirty"}"; + "org.opencontainers.image.source" = "https://github.com/TraceMachina/native-link"; + "org.opencontainers.image.title" = "Native Link"; + "org.opencontainers.image.vendor" = "Trace Machina, Inc."; + }; + }; + }; + }; checks = { # TODO(aaronmondal): Fix the tests. # tests = craneLib.cargoNextest (commonArgs @@ -74,6 +101,12 @@ pkgs.pre-commit pkgs.bazel pkgs.awscli2 + pkgs.skopeo + pkgs.dive + pkgs.cosign + + # Additional tools from within our development environment. + local-image-test ]; shellHook = '' # Generate the .pre-commit-config.yaml symlink when entering the diff --git a/tools/local-image-test.nix b/tools/local-image-test.nix new file mode 100644 index 000000000..d36d0d884 --- /dev/null +++ b/tools/local-image-test.nix @@ -0,0 +1,24 @@ +{ pkgs, ... }: + +pkgs.writeShellScriptBin "local-image-test" '' + set -xeuo pipefail + + # Commit hashes would not be a good choice here as they are not + # fully dependent on the inputs to the image. For instance, amending + # nothing would still lead to a new hash. Instead we use the + # derivation hash as the tag so that the tag is reused if the image + # didn't change. + IMAGE_TAG=$(nix eval .#image.imageTag --raw) + + $(nix build .#image --print-build-logs --verbose) \ + && ./result \ + | ${pkgs.skopeo}/bin/skopeo \ + copy \ + docker-archive:/dev/stdin \ + docker-daemon:native-link:''${IMAGE_TAG} + + # Ensure that the image has minimal closure size. + CI=1 ${pkgs.dive}/bin/dive \ + native-link:''${IMAGE_TAG} \ + --highestWastedBytes=0 +'' diff --git a/tools/publish-ghcr.nix b/tools/publish-ghcr.nix new file mode 100644 index 000000000..aa3d76c5c --- /dev/null +++ b/tools/publish-ghcr.nix @@ -0,0 +1,44 @@ +{ pkgs, ... }: + +pkgs.writeShellScriptBin "publish-ghcr" '' + set -xeuo pipefail + + echo $GHCR_PASSWORD | ${pkgs.skopeo}/bin/skopeo \ + login \ + --username=$GHCR_USERNAME \ + --password-stdin \ + ghcr.io + + # Commit hashes would not be a good choice here as they are not + # fully dependent on the inputs to the image. For instance, amending + # nothing would still lead to a new hash. Instead we use the + # derivation hash as the tag so that the tag is reused if the image + # didn't change. + IMAGE_TAG=$(nix eval .#image.imageTag --raw) + + TAGGED_IMAGE=''${GHCR_REGISTRY}/''${GHCR_IMAGE_NAME}:''${IMAGE_TAG} + + $(nix build .#image --print-build-logs --verbose) \ + && ./result \ + | ${pkgs.zstd}/bin/zstd \ + | ${pkgs.skopeo}/bin/skopeo \ + copy \ + docker-archive:/dev/stdin \ + docker://''${TAGGED_IMAGE} + + echo $GHCR_PASSWORD | ${pkgs.cosign}/bin/cosign \ + login \ + --username=$GHCR_USERNAME \ + --password-stdin \ + ghcr.io + + ${pkgs.cosign}/bin/cosign \ + sign \ + --yes \ + ''${GHCR_REGISTRY}/''${GHCR_IMAGE_NAME}@$( \ + ${pkgs.skopeo}/bin/skopeo \ + inspect \ + --format "{{ .Digest }}" \ + docker://''${TAGGED_IMAGE} \ + ) +''