diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index b7d21c93d805..48975794192b 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,9 +2,10 @@ All commands should be run from the repository root (`/ic`). -Never manually edit `ci/container/TAG`. It is bumped only by the +Never manually edit `ci/container/TAG`, `ci/container/ic-dev.digest` or +`ci/container/ic-build.digest`. They are written only by the `container-autobuild.yml` GitHub Actions workflow, which builds the new -dev-container image and pushes it to the registry *before* the tag change +dev-container image and pushes it to the registry *before* the tag and digest changes takes effect. Hand-editing it points the tag at an image that was never built or published, breaking CI for everyone. If the dev container image genuinely needs to change (e.g. `ci/container/Dockerfile` was edited), let that workflow diff --git a/.claude/skills/run-in-dev-container/SKILL.md b/.claude/skills/run-in-dev-container/SKILL.md index fbae108e6fe9..3e3b9b3d6aff 100644 --- a/.claude/skills/run-in-dev-container/SKILL.md +++ b/.claude/skills/run-in-dev-container/SKILL.md @@ -47,7 +47,12 @@ runtime's daemon isn't reachable it prints which command it tried. - The repo is mounted at `/ic` and that's the working directory, so invoke repo-local scripts with a relative path, e.g. `CONTAINER_RUNTIME=docker ./ci/container/container-run.sh ./path/to/script.sh`. -- The image is pulled from `ghcr.io` on first use (large, one-time). +- The image is pulled from `ghcr.io` on first use (large, one-time) by the + digest committed in `ci/container/ic-dev.digest`; If `ci/container/Dockerfile`, + `init.sh` or `files/*` differ from what `ci/container/TAG` was built from + (you edited them, or the autobuild's commit hasn't landed on your branch yet) + the script refuses to run; `CONTAINER_RUN_ALLOW_UNPINNED=1` makes it build the + image locally instead, which is slow. - Anything the command writes under `/ic` (or `~/.cache`) persists on the host, since those are bind-mounted. - Don't nest: the script refuses to run inside an existing container. diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 2875baeab9d5..ad07b9622ed1 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -61,6 +61,8 @@ jobs: fuzzers: - ".github/workflows/ci-main.yml" - "ci/container/TAG" + - "ci/container/ic-build.digest" + - "ci/container/ic-dev.digest" - "bin/fuzzing/*.sh" - "**/BUILD.bazel" - "**/*.bzl" @@ -822,6 +824,8 @@ jobs: - "**/*.lock" - "ci/scripts/rust-lint.sh" - "ci/container/TAG" + - "ci/container/ic-build.digest" + - "ci/container/ic-dev.digest" - ".github/**" - name: Run Cargo ${{ matrix.name }} Linux if: | @@ -868,20 +872,196 @@ jobs: with: commit-sha: ${{ github.sha }} + test-container-run-preflight: + # Decides once whether the test-container-run matrix below runs at all, so + # its legs only allocate runners when the container tooling changed (or on + # master and the release branches). + name: Test Container Run preflight + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + # 'true' only when the container tests are due and ci/container/TAG is in + # sync with the container inputs; 'false' or empty otherwise. + run: ${{ steps.sync.outputs.in_sync }} + steps: + - *checkout + - name: Filter Relevant Files + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + id: filter + with: + ref: ${{ inputs.commit-sha }} + filters: | + container-run: + - '.github/workflows/ci-main.yml' + - '.github/workflows/ci-pr-only.yml' + - '.github/workflows/container-autobuild.yml' + - 'ci/container/**' + pins: + - '.devcontainer/**' + - '.github/workflows/**' + - name: Decide whether to run + id: decide + if: | + steps.filter.outputs.container-run == 'true' || + env.BRANCH_NAME == 'master' || + startsWith(env.BRANCH_NAME, 'rc--') || + startsWith(env.BRANCH_NAME, 'hotfix-') + run: echo "run=true" >> "$GITHUB_OUTPUT" + - name: Check the digest pins are consistent + if: steps.decide.outputs.run == 'true' || steps.filter.outputs.pins == 'true' + run: | + # ci/container/*.digest, the ic-dev pin in .devcontainer/ and the ic-build + # pins in the workflows are all written by container-autobuild.yml and + # must agree. + set -euo pipefail + prefix="ghcr.io/dfinity/" + dev_digest="$(< ci/container/ic-dev.digest)" + build_digest="$(< ci/container/ic-build.digest)" + for digest in "$dev_digest" "$build_digest"; do + if ! [[ $digest =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "::error::malformed digest '$digest' in ci/container/*.digest (they are written by container-autobuild.yml)" + exit 1 + fi + done + # Every reference to either image (":tag" or "@digest", well-formed or + # not) in both consumer locations must be exactly the pinned digest + # reference, so a reference changed to a mutable tag or a malformed + # digest is rejected too, not only a mismatching well-formed one. At + # least one reference per image must exist (it does today), which also + # guards against the grep pattern going stale. + check_refs() { + local image=$1 digest=$2 refs + # grep exits 1 when nothing matches; treat that as an empty set so the + # comparison below reports it instead of pipefail aborting the step. + refs="$({ grep -rhoE "${prefix}${image}[:@][^\"'[:space:]]+" .devcontainer .github/workflows || true; } | sort -u)" + if [ "$refs" != "${prefix}${image}@${digest}" ]; then + echo "::error file=ci/container/${image}.digest::every ${image} reference in .devcontainer/ and .github/workflows/ must be ${prefix}${image}@${digest} (ci/container/${image}.digest; all are written by container-autobuild.yml), found:" + echo "${refs:-}" + exit 1 + fi + } + check_refs ic-dev "$dev_digest" + check_refs ic-build "$build_digest" + - name: Check ci/container/TAG matches the container inputs + id: sync + if: steps.decide.outputs.run == 'true' + run: | + # When TAG is out of sync there is no reviewed digest for the working + # tree and container-run.sh refuses to run (it only builds locally with + # CONTAINER_RUN_ALLOW_UNPINNED=1), so every test-container-run leg + # would fail. On a pull request that state is transient: the + # container-autobuild.yml bot commit brings TAG and *.digest in sync and + # re-triggers CI. (PRs from forks cannot change the container inputs: + # touching ci/* closes them automatically, see + # .github/repo_policies/EXTERNAL_CONTRIB_BLACKLIST.) + # Anywhere else (push to master, the merge queue, release-testing on + # rc--/hotfix-, a manual dispatch) it means that commit never landed, + # so fail. + set -euo pipefail + computed="$(ci/container/get-image-tag.sh)" + pinned="$(< ci/container/TAG)" + if [ "$computed" = "$pinned" ]; then + echo "in_sync=true" >> "$GITHUB_OUTPUT" + elif [ "$CI_EVENT_NAME" = pull_request ]; then + echo "::notice file=ci/container/TAG::ci/container/TAG ($pinned) does not match the container inputs (computed $computed); skipping the container-run tests: the Container IC Build Image workflow will commit TAG and *.digest to this branch and re-trigger CI" + echo "in_sync=false" >> "$GITHUB_OUTPUT" + else + echo "::error file=ci/container/TAG::ci/container/TAG ($pinned) does not match the container inputs (computed $computed): the container-autobuild.yml commit did not land on $BRANCH_NAME ($CI_EVENT_NAME)" + exit 1 + fi test-container-run: name: Test Container Run ${{ matrix.name }} + needs: test-container-run-preflight + if: needs.test-container-run-preflight.outputs.run == 'true' runs-on: ubuntu-latest timeout-minutes: 30 strategy: + fail-fast: false matrix: include: - - name: TestCase 1 + # Happy paths: the pinned image is pulled by digest, verified and run. + - name: ic-dev default + setup: "" command: "" - - name: TestCase 2 + expect: success + runtime: podman + - name: ic-build + setup: "" command: "--image ic-build bash" + expect: success + runtime: podman + # A local image squatting on the tag must be ignored: the pinned digest + # is pulled and run instead, and the tag is re-pointed to it (checked by + # the "Verify the poisoned tag ..." step below). The squatter is an + # empty image that cannot run anything, so a successful `true` already + # proves the pinned image ran. + - name: poisoned local tag + setup: | + tar -cf empty.tar -T /dev/null + repo=ghcr.io/dfinity + sudo podman import empty.tar "$repo/ic-dev:$(< ci/container/TAG)" + command: "true" + expect: success + runtime: podman + # A tampered pin must be refused before anything is pulled or built. + - name: malformed pin refused + setup: echo not-a-digest > ci/container/ic-dev.digest + command: "true" + expect: "refusing to pull an unpinned image" + runtime: podman + # A well-formed pin the registry does not serve: the pull fails and, by + # default, neither a local build nor a cached image is used. + - name: unknown digest refused + setup: printf 'sha256:%064d\n' 0 > ci/container/ic-dev.digest + command: "true" + expect: "refusing to build an unpinned image locally" + runtime: podman + # The same failed pull while a local image squats on the tag: the cached + # mutable-tag image must not be run either (only + # CONTAINER_RUN_ALLOW_UNPINNED=1 permits that), so this still refuses. + - name: unknown digest with poisoned local tag refused + setup: | + tar -cf empty.tar -T /dev/null + repo=ghcr.io/dfinity + sudo podman import empty.tar "$repo/ic-dev:$(< ci/container/TAG)" + printf 'sha256:%064d\n' 0 > ci/container/ic-dev.digest + command: "true" + expect: "refusing to build an unpinned image locally" + runtime: podman + # Edited container inputs (ci/container/{Dockerfile,init.sh,files/*}): no + # reviewed digest exists for the computed tag, so by default the script + # refuses without pulling or building anything. The setup step edits the + # input after the preflight job's TAG-sync gate has passed on the pristine + # checkout. + - name: edited inputs refused + setup: echo '# edited after the sync check' >> ci/container/Dockerfile + command: "true" + expect: "refusing to run an unpinned image" + runtime: podman + # With the explicit opt-out, an image cached under the computed tag is + # reused instead of being rebuilt, and the script must say that it is + # unverified. The cache is seeded by re-tagging the pinned image. + - name: edited inputs with opt-out reuses local image + setup: | + echo '# edited after the sync check' >> ci/container/Dockerfile + repo=ghcr.io/dfinity + pinned="$repo/ic-dev@$(< ci/container/ic-dev.digest)" + sudo podman pull "$pinned" + sudo podman tag "$pinned" "$repo/ic-dev:$(ci/container/get-image-tag.sh)" + command: "true" + allow_unpinned: "1" + expect: success + expect_warning: "NOT verified against a reviewed digest pin" + runtime: podman + - name: docker runtime + setup: "" + command: "true" + expect: success + runtime: docker steps: - *checkout - name: Workaround for actions/runner-images#14473 (crun pinning) + if: matrix.runtime == 'podman' run: | # Workaround for actions/runner-images#14473: # Podman 5.x on ubuntu-24.04/22.04 picks /usr/bin/crun (1.14.1) over @@ -897,21 +1077,71 @@ jobs: if command -v podman &>/dev/null && [ -x /usr/local/bin/crun ]; then echo 'PODMAN_RUN_USR_ARGS=(--runtime /usr/local/bin/crun)' >>~/.container-run.conf fi - - name: Filter Relevant Files - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 - id: filter - with: - ref: ${{ inputs.commit-sha }} - filters: | - container-run: - - '.github/workflows/ci-pr-only.yml' - - '.github/workflows/container-autobuild.yml' - - 'ci/container/**' + - name: Set up "${{ matrix.name }}" + if: matrix.setup != '' + run: ${{ matrix.setup }} - name: Test Container Run - if: | - steps.filter.outputs.container-run == 'true' || - env.BRANCH_NAME == 'master' || - startsWith(env.BRANCH_NAME, 'rc--') || - startsWith(env.BRANCH_NAME, 'hotfix-') + env: + CONTAINER_RUNTIME: ${{ matrix.runtime }} + # Empty for every leg that does not set allow_unpinned, which the script + # treats as unset. + CONTAINER_RUN_ALLOW_UNPINNED: ${{ matrix.allow_unpinned }} + run: | + set -euo pipefail + if [ "${{ matrix.expect }}" = success ]; then + set +e + out="$(./ci/container/container-run.sh ${{ matrix.command }} 2>&1)" + rc=$? + set -e + printf '%s\n' "$out" + if [ "$rc" -ne 0 ]; then + echo "::error::container-run.sh exited with $rc" + exit 1 + fi + else + # A tampered pin or edited inputs must fail fast: not succeed, not hit + # the timeout, and not fall through to a local build that merely times + # the job out. Snapshot the store first: a refusal must leave it + # untouched (nothing pulled or built) and must not have run anything, + # in particular not an image the setup step cached under a tag. + images_before="$(sudo podman images --no-trunc -q | sort -u)" + containers_before="$(sudo podman ps -aq)" + set +e + out="$(timeout 300 ./ci/container/container-run.sh ${{ matrix.command }} 2>&1)" + rc=$? + set -e + printf '%s\n' "$out" + if [ "$rc" -eq 0 ] || [ "$rc" -eq 124 ]; then + echo "::error::expected container-run.sh to refuse quickly, got exit code $rc" + exit 1 + fi + if ! grep -qF -- "${{ matrix.expect }}" <<<"$out"; then + echo "::error::expected the output to contain '${{ matrix.expect }}'" + exit 1 + fi + if [ "$(sudo podman images --no-trunc -q | sort -u)" != "$images_before" ]; then + echo "::error::an image was pulled or built despite the refusal" + exit 1 + fi + if [ "$(sudo podman ps -aq)" != "$containers_before" ]; then + echo "::error::a container was run despite the refusal" + exit 1 + fi + fi + if [ -n "${{ matrix.expect_warning }}" ] && ! grep -qF -- "${{ matrix.expect_warning }}" <<<"$out"; then + echo "::error::expected the output to contain '${{ matrix.expect_warning }}'" + exit 1 + fi + - name: Verify the poisoned tag was re-pointed to the verified image + if: matrix.name == 'poisoned local tag' run: | - ./ci/container/container-run.sh ${{ matrix.command }} + set -euo pipefail + repo=ghcr.io/dfinity + tag="$repo/ic-dev:$(< ci/container/TAG)" + want="$repo/ic-dev@$(< ci/container/ic-dev.digest)" + digests="$(sudo podman image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$tag")" + printf '%s\n' "$digests" + if ! grep -qxF "$want" <<<"$digests"; then + echo "::error::$tag does not carry the pinned digest $want after the run" + exit 1 + fi diff --git a/.github/workflows/container-autobuild.yml b/.github/workflows/container-autobuild.yml index ac5623befc7a..07613f9de950 100644 --- a/.github/workflows/container-autobuild.yml +++ b/.github/workflows/container-autobuild.yml @@ -45,7 +45,7 @@ jobs: if grep $image_tag ci/container/TAG; then echo "build_image=false" >> "$GITHUB_OUTPUT" echo "Update with this image already in this PR!" - echo "Modify ci/container/TAG with random string to trigger new build." + echo "Modify ci/container/TAG with a random string to trigger a new build." else echo "build_image=true" >> "$GITHUB_OUTPUT" echo "image_tag=$image_tag" >> "$GITHUB_OUTPUT" @@ -160,10 +160,20 @@ jobs: set -xeuo pipefail IMAGE_PREFIX="ghcr.io/dfinity/" - IMG_NAME_IC_BUILD="${IMAGE_PREFIX}ic-build@${{ needs.ic-build-image.outputs.ic-build-imagedigest }}" - IMG_NAME_IC_DEV="${IMAGE_PREFIX}ic-dev@${{ needs.ic-build-image.outputs.ic-dev-imagedigest }}" + IC_BUILD_DIGEST='${{ needs.ic-build-image.outputs.ic-build-imagedigest }}' + IC_DEV_DIGEST='${{ needs.ic-build-image.outputs.ic-dev-imagedigest }}' + for digest in "$IC_BUILD_DIGEST" "$IC_DEV_DIGEST"; do + if ! [[ $digest =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "::error::malformed image digest '$digest'" + exit 1 + fi + done + IMG_NAME_IC_BUILD="${IMAGE_PREFIX}ic-build@${IC_BUILD_DIGEST}" + IMG_NAME_IC_DEV="${IMAGE_PREFIX}ic-dev@${IC_DEV_DIGEST}" echo '${{ needs.build-image-prep.outputs.image_tag }}' > ci/container/TAG + echo "$IC_BUILD_DIGEST" > ci/container/ic-build.digest + echo "$IC_DEV_DIGEST" > ci/container/ic-dev.digest pushd .devcontainer sed -i -E "s|${IMAGE_PREFIX}ic-dev(:\|@)[^\"]{5,}|$IMG_NAME_IC_DEV|g" -- * popd diff --git a/ci/container/README.md b/ci/container/README.md index aa5b7750282b..4bfe332c1dc0 100644 --- a/ci/container/README.md +++ b/ci/container/README.md @@ -84,9 +84,17 @@ sudo podman run --pids-limit=-1 -it --rm --privileged --network=host --cgroupns= --mount type=bind,source=/home/john/.local/share/fish,target=/home/ubuntu/.local/share/fish \ --mount type=bind,source=/home/john/.zsh_history,target=/home/ubuntu/.zsh_history \ -v /tmp/ssh-XXXXQAO7kF/agent.113731:/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent -w /ic \ - ghcr.io/dfinity/ic-dev:221b79c4f4a966eae67a3f9ef7f20f4c5583d5bc38df17c94128804687a84c29 /usr/bin/fish + --pull=never ghcr.io/dfinity/ic-dev@$(< ci/container/ic-dev.digest) /usr/bin/fish ``` +### Image pinning + +`container-run.sh` never pulls an image by registry tag: the tags on `ghcr.io/dfinity/ic-dev` and `ghcr.io/dfinity/ic-build` are mutable and can be re-pointed without review. Instead it pulls `ghcr.io/dfinity/@sha256:...` using the digest committed in [ic-dev.digest](ic-dev.digest) / [ic-build.digest](ic-build.digest), which the container runtime verifies against the downloaded content, and starts exactly that reference with `--pull=never`. Like [TAG](TAG), the `.digest` files are written only by the `container-autobuild.yml` workflow. Never edit them by hand: a malformed pin makes the script refuse to run, and a digest the registry does not serve cannot be pulled, so the script refuses as well. + +`TAG` is the hash of `Dockerfile`, `init.sh` and `files/*`. If your working tree hashes to something else (you edited one of those files, or the autobuild's bot commit has not landed on your branch yet) no reviewed digest exists for it and the script refuses to run. To build the image locally from your checkout instead (slow, and it needs network), set `CONTAINER_RUN_ALLOW_UNPINNED=1`; an existing local `:` is then reused. Once the bot commit lands, `git pull` and re-run to get the registry image. The autobuild cannot publish for pull requests from forks, so contributors working on a fork need `CONTAINER_RUN_ALLOW_UNPINNED=1` after touching those files. + +By default the script never runs an image that is not verified against the pin: no local build, and no reuse of a cached image when the pinned pull fails (e.g. offline). `CONTAINER_RUN_ALLOW_UNPINNED=1` opts out of that for one run and is announced with a warning; leave it unset when verifying release artifacts. + ### How to use custom config User can create config `$HOME/.container-run.conf`, with `podman run` arguments, that provide way of adding custom bind-mounts etc. Config file requires array variable `PODMAN_RUN_USR_ARGS` with arguments accepted by `podman run` (see `podman run --help`). See example config from `.container-run.conf` below: diff --git a/ci/container/container-run.sh b/ci/container/container-run.sh index 0ca09a2b7d13..2861ea69f5c8 100755 --- a/ci/container/container-run.sh +++ b/ci/container/container-run.sh @@ -108,21 +108,92 @@ if ! "${CONTAINER_CMD[@]}" info >/dev/null 2>&1; then exit 1 fi +die() { + warn "$@" + exit 1 +} + +case "$IMAGE_NAME" in + ic-dev | ic-build) ;; + *) die "Unknown image '$IMAGE_NAME' (expected ic-dev or ic-build)" ;; +esac + REPO_ROOT="$(git rev-parse --show-toplevel)" -IMAGE_TAG=$("$REPO_ROOT"/ci/container/get-image-tag.sh) -IMAGE="ghcr.io/dfinity/$IMAGE_NAME:$IMAGE_TAG" +IMAGE_REPO="ghcr.io/dfinity/$IMAGE_NAME" +IMAGE_TAG="$("$REPO_ROOT"/ci/container/get-image-tag.sh)" +PINNED_TAG="$(<"$REPO_ROOT/ci/container/TAG")" +DIGEST_FILE="$REPO_ROOT/ci/container/$IMAGE_NAME.digest" +# By default the script refuses to run any image that is not verified against +# the committed digest pin. CONTAINER_RUN_ALLOW_UNPINNED=1 opts out for this run: +# build the image locally from the checkout when no reviewed pin exists for the +# working tree, or reuse an existing local image when the pinned pull fails. +# Only the exact value 1 opts out, so that 0, false or a typo cannot silently +# disable the check. +case "${CONTAINER_RUN_ALLOW_UNPINNED:-}" in + "") ALLOW_UNPINNED="" ;; + 1) ALLOW_UNPINNED=1 ;; + *) die "CONTAINER_RUN_ALLOW_UNPINNED must be unset or exactly '1' (got '${CONTAINER_RUN_ALLOW_UNPINNED}')" ;; +esac +# Set when the image about to run is NOT verified against a reviewed digest pin +# (built locally, or an existing local image reused because the pull failed). +LOCAL_IMAGE=false + +image_exists() { + if [ "$RUNTIME" = docker ]; then + "${CONTAINER_CMD[@]}" image inspect "$1" >/dev/null 2>&1 + else + "${CONTAINER_CMD[@]}" image exists "$1" + fi +} -# Check for a locally-available image (podman has `image exists`; docker doesn't, -# so use `image inspect`) and pull or build it if it's missing. -if [ "$RUNTIME" = docker ]; then - image_exists_cmd=("${CONTAINER_CMD[@]}" image inspect "$IMAGE") +build_locally() { + if [ -z "$ALLOW_UNPINNED" ]; then + die "$1; refusing to build an unpinned image locally (set CONTAINER_RUN_ALLOW_UNPINNED=1 to run an image that is not verified against the committed digest)" + fi + warn "$1; building $IMAGE_REPO:$IMAGE_TAG locally from the checkout (this takes a while)" + "$REPO_ROOT"/ci/container/build-image.sh --image "$IMAGE_NAME" --container-cmd "${CONTAINER_CMD[*]}" + IMAGE="$IMAGE_REPO:$IMAGE_TAG" + LOCAL_IMAGE=true +} + +if [ "$IMAGE_TAG" = "$PINNED_TAG" ]; then + [ -f "$DIGEST_FILE" ] || die "$DIGEST_FILE is missing: refusing to pull an unpinned image" + IMAGE_DIGEST="$(<"$DIGEST_FILE")" + if ! [[ $IMAGE_DIGEST =~ ^sha256:[0-9a-f]{64}$ ]]; then + die "$DIGEST_FILE is malformed ('$IMAGE_DIGEST'): refusing to pull an unpinned image" + fi + IMAGE="$IMAGE_REPO@$IMAGE_DIGEST" + if ! image_exists "$IMAGE" && image_exists "$IMAGE_REPO:$IMAGE_TAG"; then + eprintln "Local image $IMAGE_REPO:$IMAGE_TAG is not verified against the pin (e.g. built locally); pulling the pinned image $IMAGE instead (one-time download)." + fi + if image_exists "$IMAGE" || "${CONTAINER_CMD[@]}" pull "$IMAGE"; then + "${CONTAINER_CMD[@]}" tag "$IMAGE" "$IMAGE_REPO:$IMAGE_TAG" \ + || die "Failed to tag the verified image $IMAGE as $IMAGE_REPO:$IMAGE_TAG" + elif [ -n "$ALLOW_UNPINNED" ] && image_exists "$IMAGE_REPO:$IMAGE_TAG"; then + warn "Pinned image $IMAGE could not be pulled (offline?); reusing the existing local image $IMAGE_REPO:$IMAGE_TAG, which is NOT verified against the pin (CONTAINER_RUN_ALLOW_UNPINNED is set)." + IMAGE="$IMAGE_REPO:$IMAGE_TAG" + LOCAL_IMAGE=true + else + build_locally "Pinned image $IMAGE could not be pulled" + fi else - image_exists_cmd=("${CONTAINER_CMD[@]}" image exists "$IMAGE") -fi -if ! "${image_exists_cmd[@]}" >/dev/null 2>&1; then - if ! "${CONTAINER_CMD[@]}" pull "$IMAGE"; then - "$REPO_ROOT"/ci/container/build-image.sh --image "$IMAGE_NAME" + warn "ci/container/{Dockerfile,init.sh,files/*} differ from what ci/container/TAG was built from:" + warn " computed tag: $IMAGE_TAG" + warn " pinned tag: $PINNED_TAG" + warn "No reviewed digest pin exists for the computed tag, and registry tags are mutable and unverified, so it is never pulled." + warn "If you pushed this change, wait for the 'Container IC Build Image' workflow to commit TAG and *.digest to your branch, then 'git pull' and re-run." + if [ -z "$ALLOW_UNPINNED" ]; then + die "No reviewed digest pin exists for tag $IMAGE_TAG; refusing to run an unpinned image (set CONTAINER_RUN_ALLOW_UNPINNED=1 to build it locally from the checkout, or to reuse an existing local $IMAGE_REPO:$IMAGE_TAG)" fi + IMAGE="$IMAGE_REPO:$IMAGE_TAG" + image_exists "$IMAGE" || build_locally "No reviewed digest pin exists for tag $IMAGE_TAG" + LOCAL_IMAGE=true +fi + +if [ "$LOCAL_IMAGE" = true ]; then + warn "Using image $IMAGE (NOT verified against a reviewed digest pin; CONTAINER_RUN_ALLOW_UNPINNED is set)" +else + eprintln "Using image $IMAGE (verified against ci/container/$IMAGE_NAME.digest)" fi # On the devenv we issue a warning if the images start taking up a lot of space. @@ -298,4 +369,4 @@ if [ -f "$HOME/.container-run.conf" ]; then fi set -x -exec "${CONTAINER_CMD[@]}" run "${RUNTIME_RUN_ARGS[@]}" "$IMAGE" "${cmd[@]}" +exec "${CONTAINER_CMD[@]}" run "${RUNTIME_RUN_ARGS[@]}" --pull=never "$IMAGE" "${cmd[@]}" diff --git a/ci/container/ic-build.digest b/ci/container/ic-build.digest new file mode 100644 index 000000000000..bea6023e247f --- /dev/null +++ b/ci/container/ic-build.digest @@ -0,0 +1 @@ +sha256:caacdcf7c2346ff8b6d8b86ecfa89b1365fa111f744e0e777ac5bf9a5ca97c61 diff --git a/ci/container/ic-dev.digest b/ci/container/ic-dev.digest new file mode 100644 index 000000000000..8c57f46f28f7 --- /dev/null +++ b/ci/container/ic-dev.digest @@ -0,0 +1 @@ +sha256:3e028a468490dddda2b255da986c8ea2fe9c2dd2a143d4557c9bd5bb68c8218a diff --git a/ci/scripts/targets.py b/ci/scripts/targets.py index f37324cbd11c..76e146a5beaa 100755 --- a/ci/scripts/targets.py +++ b/ci/scripts/targets.py @@ -53,6 +53,8 @@ "*.bzl", "bazel/*", "ci/container/TAG", + "ci/container/ic-build.digest", + "ci/container/ic-dev.digest", "mainnet-*-revisions.json", ]