From b45c4452557402366dfb0168b38a47dae422ae21 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Tue, 7 Feb 2023 03:30:35 +1100 Subject: [PATCH] feat: add git to Docker image (#6034) --- .github/workflows/docker-base.yaml | 53 ++++++++++++++++++++++++ .github/workflows/release.yaml | 65 +++++++++++++++++++++++------- .github/workflows/security.yaml | 16 +++++++- Dockerfile | 16 +++----- Dockerfile.base | 27 +++++++++++++ dogfood/Dockerfile | 6 +++ scripts/build_docker.sh | 56 ++++++++++++++++--------- scripts/notarize_darwin.sh | 5 ++- 8 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/docker-base.yaml create mode 100644 Dockerfile.base diff --git a/.github/workflows/docker-base.yaml b/.github/workflows/docker-base.yaml new file mode 100644 index 0000000000..f06ab7409a --- /dev/null +++ b/.github/workflows/docker-base.yaml @@ -0,0 +1,53 @@ +name: docker-base + +on: + push: + branches: + - main + paths: + - Dockerfile.base + - Dockerfile + + schedule: + # Run every week at 09:43 on Monday, Wednesday and Friday. We build this + # frequently to ensure that packages are up-to-date. + - cron: "43 9 * * 1,3,5" + + workflow_dispatch: + +# Avoid running multiple jobs for the same commit. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-docker-base + +jobs: + build: + runs-on: ubuntu-latest + if: github.repository_owner == 'coder' + steps: + - uses: actions/checkout@v3 + + - name: Docker login + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create empty base-build-context directory + run: mkdir base-build-context + + - name: Install depot.dev CLI + uses: depot/setup-action@v1 + + # This uses OIDC authentication, so no auth variables are required. + - name: Build base Docker image via depot.dev + uses: depot/build-push-action@v1 + with: + project: wl5hnrrkns + context: base-build-context + file: Dockerfile.base + pull: true + no-cache: true + push: true + tags: | + ghcr.io/coder/coder-base:latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 11544ee6cb..a2bfe09a53 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -112,17 +112,17 @@ jobs: set -euo pipefail wget -O /tmp/nfpm.deb https://github.com/goreleaser/nfpm/releases/download/v2.18.1/nfpm_amd64.deb sudo dpkg -i /tmp/nfpm.deb + rm /tmp/nfpm.deb - name: Install rcodesign run: | set -euo pipefail - - # Install a prebuilt binary of rcodesign for linux amd64. Once the - # following PR is merged and released upstream, we can download - # directly from GitHub releases instead: - # https://github.com/indygreg/PyOxidizer/pull/635 - wget -O /tmp/rcodesign https://cdn.discordapp.com/attachments/283356472258199552/1016767245717872700/rcodesign - sudo install --mode 755 /tmp/rcodesign /usr/local/bin/rcodesign + wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-x86_64-unknown-linux-musl.tar.gz + sudo tar -xzf /tmp/rcodesign.tar.gz \ + -C /usr/bin \ + --strip-components=1 \ + apple-codesign-0.22.0-x86_64-unknown-linux-musl/rcodesign + rm /tmp/rcodesign.tar.gz - name: Setup Apple Developer certificate and API key run: | @@ -160,6 +160,39 @@ jobs: - name: Delete Apple Developer certificate and API key run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} + - name: Determine base image tag + id: image-base-tag + run: | + set -euo pipefail + if [[ "${CODER_RELEASE:-}" != *t* ]] || [[ "${CODER_DRY_RUN:-}" == *t* ]]; then + # Empty value means use the default and avoid building a fresh one. + echo "tag=" >> $GITHUB_OUTPUT + else + echo "tag=$(CODER_IMAGE_BASE=ghcr.io/coder/coder-base ./scripts/image_tag.sh)" >> $GITHUB_OUTPUT + fi + + - name: Create empty base-build-context directory + if: steps.image-base-tag.outputs.tag != '' + run: mkdir base-build-context + + - name: Install depot.dev CLI + if: steps.image-base-tag.outputs.tag != '' + uses: depot/setup-action@v1 + + # This uses OIDC authentication, so no auth variables are required. + - name: Build base Docker image via depot.dev + if: steps.image-base-tag.outputs.tag != '' + uses: depot/build-push-action@v1 + with: + project: wl5hnrrkns + context: base-build-context + file: Dockerfile.base + pull: true + no-cache: true + push: true + tags: | + ${{ steps.image-base-tag.outputs.tag }} + - name: Build Linux Docker images run: | set -euxo pipefail @@ -188,6 +221,8 @@ jobs: --target "$(./scripts/image_tag.sh --version latest)" \ $(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag) fi + env: + CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }} - name: ls build run: ls -lh build @@ -252,6 +287,14 @@ jobs: ./build/*.rpm retention-days: 7 + - name: Start Packer builds + uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.CDRCI_GITHUB_TOKEN }} + repository: coder/packages + event-type: coder-release + client-payload: '{"coder_version": "${{ steps.version.outputs.version }}"}' + publish-winget: name: Publish to winget-pkgs runs-on: windows-latest @@ -333,11 +376,3 @@ jobs: # For gh CLI. We need a real token since we're commenting on a PR in a # different repo. GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }} - - - name: Start Packer builds - uses: peter-evans/repository-dispatch@v2 - with: - token: ${{ secrets.CDRCI_GITHUB_TOKEN }} - repository: coder/packages - event-type: coder-release - client-payload: '{"coder_version": "${{ needs.release.outputs.version }}"}' diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 15438f2eed..4e00e8ff33 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -96,8 +96,20 @@ jobs: id: build run: | set -euo pipefail - image_job="build/coder_$(./scripts/version.sh)_linux_amd64.tag" - DOCKER_IMAGE_NO_PREREQUISITES=true make -j "$image_job" + + version="$(./scripts/version.sh)" + image_job="build/coder_${version}_linux_amd64.tag" + + # This environment variable force make to not build packages and + # archives (which the Docker image depends on due to technical reasons + # related to concurrent FS writes). + export DOCKER_IMAGE_NO_PREREQUISITES=true + # This environment variables forces scripts/build_docker.sh to build + # the base image tag locally instead of using the cached version from + # the registry. + export CODER_IMAGE_BUILD_BASE_TAG="$(CODER_IMAGE_BASE=coder-base ./scripts/image_tag.sh --version "$version")" + + make -j "$image_job" echo "image=$(cat "$image_job")" >> $GITHUB_OUTPUT - name: Run Trivy vulnerability scanner diff --git a/Dockerfile b/Dockerfile index 36521ef19b..b36de3ec45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,11 @@ # cross-compiled, it cannot have ANY "RUN" commands. All binaries are built # using the go toolchain on the host and then copied into the build context by # scripts/build_docker.sh. -FROM alpine:latest +# +# If you need RUN commands (e.g. to install tools via apk), add them to +# Dockerfile.base instead, which supports "RUN" commands. +ARG BASE_IMAGE +FROM $BASE_IMAGE # LABEL doesn't add any real layers so it's fine (and easier) to do it here than # in the build script. @@ -14,17 +18,7 @@ LABEL \ org.opencontainers.image.source="https://github.com/coder/coder" \ org.opencontainers.image.version="$CODER_VERSION" -# Create coder group and user. We cannot use `addgroup` and `adduser` because -# they won't work if we're building the image for a different architecture. -COPY --chown=0:0 --chmod=644 group passwd /etc/ -COPY --chown=1000:1000 --chmod=700 empty-dir /home/coder - # The coder binary is injected by scripts/build_docker.sh. COPY --chown=1000:1000 --chmod=755 coder /opt/coder -USER 1000:1000 -ENV HOME=/home/coder -ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt -WORKDIR /home/coder - ENTRYPOINT [ "/opt/coder", "server" ] diff --git a/Dockerfile.base b/Dockerfile.base new file mode 100644 index 0000000000..8d926fe8ea --- /dev/null +++ b/Dockerfile.base @@ -0,0 +1,27 @@ +# This is the base image used for Coder images. It's a multi-arch image that is +# built in depot.dev for all supported architectures. Since it's built on real +# hardware and not cross-compiled, it can have "RUN" commands. +FROM alpine:latest + +# We use a single RUN command to reduce the number of layers in the image. +RUN apk add --no-cache \ + curl \ + wget \ + bash \ + git \ + openssh-client && \ + addgroup \ + -g 1000 \ + coder && \ + adduser \ + -D \ + -s /bin/bash \ + -h /home/coder \ + -u 1000 \ + -G coder \ + coder + +USER 1000:1000 +ENV HOME=/home/coder +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt +WORKDIR /home/coder diff --git a/dogfood/Dockerfile b/dogfood/Dockerfile index 73639e361f..8e2717405d 100644 --- a/dogfood/Dockerfile +++ b/dogfood/Dockerfile @@ -164,6 +164,12 @@ RUN apt-get update --quiet && apt-get install --yes \ # Configure FIPS-compliant policies update-crypto-policies --set FIPS +# Install the docker buildx component. +RUN DOCKER_BUILDX_VERSION=$(curl -s "https://api.github.com/repos/docker/buildx/releases/latest" | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\1/') && \ + mkdir -p /usr/local/lib/docker/cli-plugins && \ + curl -Lo /usr/local/lib/docker/cli-plugins/docker-buildx "https://github.com/docker/buildx/releases/download/${DOCKER_BUILDX_VERSION}/buildx-${DOCKER_BUILDX_VERSION}.linux-amd64" && \ + chmod a+x /usr/local/lib/docker/cli-plugins/docker-buildx + # See https://github.com/cli/cli/issues/6175#issuecomment-1235984381 for proof # the apt repository is unreliable RUN curl -L https://github.com/cli/cli/releases/download/v2.14.7/gh_2.14.7_linux_amd64.deb -o gh.deb && \ diff --git a/scripts/build_docker.sh b/scripts/build_docker.sh index 06b1b2fbe6..761646e8f9 100755 --- a/scripts/build_docker.sh +++ b/scripts/build_docker.sh @@ -3,12 +3,19 @@ # This script builds a Docker image of Coder containing the given binary, for # the given architecture. Only linux binaries are supported at this time. # -# Usage: ./build_docker.sh --arch amd64 [--version 1.2.3] [--target image_tag] [--push] path/to/coder +# Usage: ./build_docker.sh --arch amd64 [--version 1.2.3] [--target image_tag] [--build-base image_tag] [--push] path/to/coder # # The --arch parameter is required and accepts a Golang arch specification. It # will be automatically mapped to a suitable architecture that Docker accepts # before being passed to `docker buildx build`. # +# The --build-base parameter is optional and specifies to build the base image +# in Dockerfile.base instead of pulling a copy from the registry. The string +# value is the tag to use for the built image (not pushed). This also consumes +# $CODER_IMAGE_BUILD_BASE_TAG for easily forcing a fresh build in CI. +# +# The default base image can be controlled via $CODER_BASE_IMAGE_TAG. +# # The image will be built and tagged against the image tag returned by # ./image_tag.sh unless a --target parameter is supplied. # @@ -22,12 +29,15 @@ set -euo pipefail # shellcheck source=scripts/lib.sh source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +DEFAULT_BASE="${CODER_BASE_IMAGE_TAG:-ghcr.io/coder/coder-base:latest}" + arch="" image_tag="" +build_base="${CODER_IMAGE_BUILD_BASE_TAG:-}" version="" push=0 -args="$(getopt -o "" -l arch:,target:,version:,push -- "$@")" +args="$(getopt -o "" -l arch:,target:,build-base:,version:,push -- "$@")" eval set -- "$args" while true; do case "$1" in @@ -43,6 +53,10 @@ while true; do version="$2" shift 2 ;; + --build-base) + build_base="$2" + shift + ;; --push) push=1 shift @@ -98,31 +112,37 @@ fi cdroot temp_dir="$(TMPDIR="$(dirname "$input_file")" mktemp -d)" ln "$input_file" "$temp_dir/coder" +ln Dockerfile.base "$temp_dir/" ln Dockerfile "$temp_dir/" cd "$temp_dir" +export DOCKER_BUILDKIT=1 + +base_image="$DEFAULT_BASE" +if [[ "$build_base" != "" ]]; then + log "--- Building base Docker image for $arch ($build_base)" + docker build \ + --platform "$arch" \ + --tag "$build_base" \ + --no-cache \ + -f Dockerfile.base \ + . 1>&2 + + base_image="$build_base" +else + docker pull --platform "$arch" "$base_image" 1>&2 +fi + log "--- Building Docker image for $arch ($image_tag)" -# Pull the base image, copy the /etc/group and /etc/passwd files out of it, and -# add the coder group and user. We have to do this in a separate step instead of -# using the RUN directive in the Dockerfile because you can't use RUN if you're -# building the image for a different architecture than the host. -docker pull --platform "$arch" alpine:latest 1>&2 - -temp_container_id="$(docker create --platform "$arch" alpine:latest)" -docker cp "$temp_container_id":/etc/group ./group 1>&2 -docker cp "$temp_container_id":/etc/passwd ./passwd 1>&2 -docker rm "$temp_container_id" 1>&2 - -echo "coder:x:1000:coder" >>./group -echo "coder:x:1000:1000::/:/bin/sh" >>./passwd -mkdir ./empty-dir - -docker buildx build \ +docker build \ --platform "$arch" \ + --build-arg "BASE_IMAGE=$base_image" \ --build-arg "CODER_VERSION=$version" \ + --no-cache \ --tag "$image_tag" \ + -f Dockerfile \ . 1>&2 cdroot diff --git a/scripts/notarize_darwin.sh b/scripts/notarize_darwin.sh index fa02d8d614..88fcad768a 100755 --- a/scripts/notarize_darwin.sh +++ b/scripts/notarize_darwin.sh @@ -47,8 +47,9 @@ rcodesign encode-app-store-connect-api-key \ # The notarization process is very fragile and heavily dependent on Apple's # notarization server not returning server errors, so we retry this step twice # with a delay of 30 seconds between attempts. +NOTARY_SUBMIT_ATTEMPTS=2 rc=0 -for i in $(seq 1 2); do +for i in $(seq 1 $NOTARY_SUBMIT_ATTEMPTS); do # -v is quite verbose, the default output is pretty good on it's own. Adding # -v makes it dump the credentials used for uploading to Apple's S3 bucket. rcodesign notary-submit \ @@ -58,7 +59,7 @@ for i in $(seq 1 2); do 1>&2 && rc=0 && break || rc=$? log "rcodesign exit code: $rc" - if [[ $i -lt 5 ]]; then + if [[ $i -lt $NOTARY_SUBMIT_ATTEMPTS ]]; then log log "Retrying notarization in 30 seconds" log