diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..c8175dc27 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,155 @@ +name: docker +on: + push: + # TODO: Build on main? + branches: + - main + tags: + - '**' + # TODO: Remove pull_request before merging + pull_request: + branches: + - main + +permissions: + contents: read + packages: write + # Ensure cosign can request for Github's OIDC JWT ID token + # See: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings + id-token: write + +jobs: + # This job builds the binaries and uploads it as github artifacts. + # This allows us to use the same binaries for any release CI jobs. + build: + name: Build all linux architectures + runs-on: ubuntu-latest + steps: + - name: setup go + uses: actions/setup-go@v3 + with: + go-version: ${{ env.GO_VERSION }} + + - uses: actions/checkout@v3 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Build on all supported architectures + run: | + set -e + ./scripts/release.sh + + - uses: actions/upload-artifact@v3 + with: + name: binaries-${{ github.sha }} + path: | + release*/* + + # This job downloads the binaries previously uploaded as artifacts, and builds multi-arch images + build-docker-image: + needs: [build] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + # Fetch all tags + fetch-depth: 0 + + - name: Prepare + id: prep + run: | + set -e + TAG=$( git describe --tags --dirty ) # E.g. v1.2.0-23-g60ee190 + echo "TAG=$TAG" >> $GITHUB_ENV + + # This step generates the docker tags + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} + # type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123', 'pr-123-abc0123' + # type=ref,event=branch generates tag(s) on branch only. E.g. 'main-abc0123' + # type=semver generates tag(s) on tags only. E.g. 'v0', 'v0.0', 'v0.0.0', and 'latest' + tags: | + type=ref,event=pr + type=ref,suffix=-{{sha}},event=branch + type=semver,pattern=v{{major}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}}.{{minor}}.{{patch}} + # The rest of the org.opencontainers.image.xxx labels are dynamically generated + labels: | + org.opencontainers.image.description=CNI Plugins + org.opencontainers.image.licenses=Apache License 2.0 + + # See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to Docker Hub registry + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/download-artifact@v3 + with: + name: binaries-${{ github.sha }} + + - run: | + ls -al release*/ + + - name: Build and push + id: build-and-push + # TODO: Remove pull_request before merging + if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') + uses: docker/build-push-action@v3 + with: + build-args: | + TAG=${{ env.TAG }} + context: '.' + file: Dockerfile + platforms: linux/amd64,linux/arm,linux/arm64,linux/mips64le,linux/ppc64le,linux/riscv64,linux/s390x + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max + + - name: Install cosign + uses: sigstore/cosign-installer@c3667d99424e7e6047999fb6246c0da843953c65 # v3.0.1 + + # This signs the image using ACTIONS_ID_TOKEN_REQUEST_TOKEN + - name: Sign the published Docker image + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes {}@${{ steps.build-and-push.outputs.digest }} + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..d595ec166 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM busybox as test + +ADD docker-installer/install_cni_plugins.sh /script/install_cni_plugins.sh +ADD docker-installer/test_install_cni_plugins.sh /script/test_install_cni_plugins.sh +WORKDIR /script +RUN /script/test_install_cni_plugins.sh + +FROM busybox as build +ARG TAG +# Get buildx automatic platform vars: https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope +ARG TARGETPLATFORM +ARG TARGETOS +ARG TARGETARCH +ARG TARGETVARIANT +ARG BUILDPLATFORM +ARG BUILDOS +ARG BUILDARCH +ARG BUILDVARIANT +RUN echo TARGETPLATFORM=$TARGETPLATFORM +RUN echo TARGETOS=$TARGETOS +RUN echo TARGETARCH=$TARGETARCH +RUN echo TARGETVARIANT=$TARGETVARIANT +RUN echo BUILDPLATFORM=$BUILDPLATFORM +RUN echo BUILDOS=$BUILDOS +RUN echo BUILDARCH=$BUILDARCH +RUN echo BUILDVARIANT=$BUILDVARIANT +# Use buildx automatic platform vars +COPY release-$TAG/cni-plugins-$TARGETOS-$TARGETARCH-$TAG.tgz cni-plugins-$TARGETOS-$TARGETARCH-$TAG.tgz +COPY release-$TAG/cni-plugins-$TARGETOS-$TARGETARCH-$TAG.tgz.sha512 cni-plugins-$TARGETOS-$TARGETARCH-$TAG.tgz.sha512 +RUN set -eux; \ + sha512sum -c cni-plugins-$TARGETOS-$TARGETARCH-$TAG.tgz.sha512; \ + mkdir -p /opt/cni/bin; \ + tar -xvf cni-plugins-$TARGETOS-$TARGETARCH-$TAG.tgz -C /opt/cni/bin; + +# This is the final, minimal container +FROM busybox as final +COPY docker-installer/install_cni_plugins.sh /script/install_cni_plugins.sh +COPY --from=build /opt/cni/bin /opt/cni/bin +ENV FORCE= +WORKDIR /opt/cni/bin +VOLUME /host/opt/cni/bin +CMD ["/script/install_cni_plugins.sh","/opt/cni/bin","/host/opt/cni/bin"] diff --git a/docker-installer/README.md b/docker-installer/README.md new file mode 100644 index 000000000..212c0084f --- /dev/null +++ b/docker-installer/README.md @@ -0,0 +1,9 @@ +This scripts are used in the docker image. + +The docker image installs the plug-ins to /host/opt/cni/bin which should be bind-mounted to /opt/cni/bin on the host. + +The installer-script keeps track of plug-ins it installs and ensures: +- that no existing plug-ins that have been installed or updated by someone else are overwriten +- that plug-ins installed by this script are updated if a new version of this image is used + +A unit-test shell script for the installer script is part of the docker build process. diff --git a/docker-installer/install_cni_plugins.sh b/docker-installer/install_cni_plugins.sh new file mode 100755 index 000000000..55e619d61 --- /dev/null +++ b/docker-installer/install_cni_plugins.sh @@ -0,0 +1,69 @@ +#!/bin/sh + +set -eu + +if [ $# -ne 2 ]; then + echo "USAGE: $0 source-dir dest-dir" + echo "Env vars:" + echo " FORCE Overwrite existing binaries" + exit 1 +fi + +SRC=$1 +DST=$2 +FORCE=${FORCE:-} + +install_cni_plugin() { + NAME=$1 + MD5=$( md5sum $SRC/$NAME | awk '{ print $1 }' ) + if [ -e $DST/$NAME ]; then + if [ ! -e $DST/$NAME.by_cni_installer_image ]; then + # The file already exists but there's no marker that + # it was installed by this installer -> keep untouched + if [ -z "$FORCE" ]; then + echo "* '$NAME' ignored (exists but not installed by me)" + return + fi + else + OTHER_MD5=$( md5sum $DST/$NAME | awk '{ print $1 }' ) + INSTALLED_MD5=$( cat $DST/$NAME.by_cni_installer_image ) + + if [ "$OTHER_MD5" != "$INSTALLED_MD5" ]; then + # The file was previously installed by this installer + # but later changed -> keep untouched + if [ -z "$FORCE" ]; then + echo "* '$NAME' ignored (previously installed by me but changed by someone else)" + return + fi + fi + + if [ "$OTHER_MD5" == "$MD5" ]; then + # The file was previously installed by this installer + # and is up-to-date + echo "* '$NAME' is up-to-date" + return + fi + fi + + # The file was previously installed by this installer + # but needs an update + cp -a $SRC/$NAME $DST/.$NAME.new + mv $DST/.$NAME.new $DST/$NAME + echo $MD5 > $DST/$NAME.by_cni_installer_image + echo "* '$NAME' updated" + + else + cp -a $SRC/$NAME $DST/.$NAME.new + mv $DST/.$NAME.new $DST/$NAME + echo $MD5 > $DST/$NAME.by_cni_installer_image + echo "* '$NAME' installed" + fi +} + +echo "Installing CNI plug-ins to $DST" +echo + +for FILE in $( find $SRC -maxdepth 1 -type f ); do + NAME=$( basename $FILE ) + install_cni_plugin $NAME +done diff --git a/docker-installer/test_install_cni_plugins.sh b/docker-installer/test_install_cni_plugins.sh new file mode 100755 index 000000000..ae0697f0d --- /dev/null +++ b/docker-installer/test_install_cni_plugins.sh @@ -0,0 +1,152 @@ +#!/bin/sh + +set -e + +prepare() { + echo + echo + echo "=== Testing: $1 ===" + echo + rm -rf test + mkdir -p test/src + mkdir -p test/dst +} + +assert_file_missing() { + FILE=test/dst/$1 + + if [ -e $FILE ]; then + echo "File $1 exists but must not exist." + echo + echo "=== Test failed. ===" + exit 1 + fi +} + +assert_file_content() { + FILE=test/dst/$1 + + if [ ! -e $FILE ]; then + echo "Expected file $1 is missing." + echo + echo "=== Test failed. ===" + exit 1 + fi + + EXPECTED_CONTENT=$2 + ACTUAL_CONTENT=$( cat $FILE ) + + if [ "$EXPECTED_CONTENT" != "$ACTUAL_CONTENT" ]; then + echo "File $1 has wrong content" + echo "Expected: $EXPECTED_CONTENT" + echo "Actual : $ACTUAL_CONTENT" + echo + echo "=== Test failed. ===" + exit 1 + fi +} + + +############################################################# + +prepare "Installation if not exists yet" + +echo "cni1.0" > test/src/cni1 +./install_cni_plugins.sh test/src test/dst + +assert_file_content cni1 "cni1.0" +assert_file_content cni1.by_cni_installer_image "7ec08d6ee0e5237cb34be4a31d9146c1" + +echo +echo "Test passed." + +############################################################# + +prepare "Keep when up-to-date" + +echo "cni1.0" > test/src/cni1 +./install_cni_plugins.sh test/src test/dst +echo "cni1.0" > test/src/cni1 +./install_cni_plugins.sh test/src test/dst + +assert_file_content cni1 "cni1.0" +assert_file_content cni1.by_cni_installer_image "7ec08d6ee0e5237cb34be4a31d9146c1" + +echo +echo "Test passed." + +############################################################# + +prepare "Upgrade when installed by this installer" + +echo "cni1.0" > test/src/cni1 +./install_cni_plugins.sh test/src test/dst + +echo "cni1.1" > test/src/cni1 +./install_cni_plugins.sh test/src test/dst + +assert_file_content cni1 "cni1.1" +assert_file_content cni1.by_cni_installer_image "286f99e881938508552bb58ea1c2c565" + +echo +echo "Test passed." + +############################################################# + +prepare "Installed by someone else" + +echo "cni1.0" > test/src/cni1 +echo "cni1.other" > test/dst/cni1 +./install_cni_plugins.sh test/src test/dst + +assert_file_content cni1 "cni1.other" +assert_file_missing cni1.by_cni_installer_image + +echo +echo "Test passed." + +############################################################# + +prepare "Installed by me, altered by someone else" + +echo "cni1.0" > test/src/cni1 +./install_cni_plugins.sh test/src test/dst + +echo "cni1.other" > test/dst/cni1 +./install_cni_plugins.sh test/src test/dst + +assert_file_content cni1 "cni1.other" +assert_file_content cni1.by_cni_installer_image "7ec08d6ee0e5237cb34be4a31d9146c1" + +echo +echo "Test passed." + +############################################################# + +prepare "Force install overwrites plugin installed by someone else" + +echo "cni1.other" > test/dst/cni1 +echo "cni1.0" > test/src/cni1 +FORCE=1 ./install_cni_plugins.sh test/src test/dst + +assert_file_content cni1 "cni1.0" +assert_file_content cni1.by_cni_installer_image "7ec08d6ee0e5237cb34be4a31d9146c1" + +prepare "Force install overwrites plugin installed by me, altered by someone else" + +echo "cni1.0" > test/src/cni1 +./install_cni_plugins.sh test/src test/dst +echo "cni1.other" > test/dst/cni1 + +FORCE=1 ./install_cni_plugins.sh test/src test/dst + +assert_file_content cni1 "cni1.0" +assert_file_content cni1.by_cni_installer_image "7ec08d6ee0e5237cb34be4a31d9146c1" + +echo +echo "Test passed." + +############################################################# + +echo +echo "All tests passed." diff --git a/scripts/release.sh b/scripts/release.sh index 5002428ed..7d6b1e169 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -16,7 +16,7 @@ rm -Rf ${SRC_DIR}/${RELEASE_DIR} mkdir -p ${SRC_DIR}/${RELEASE_DIR} mkdir -p ${OUTPUT_DIR} -$DOCKER run -ti -v ${SRC_DIR}:/go/src/github.com/containernetworking/plugins:z --rm golang:1.20-alpine \ +$DOCKER run -v ${SRC_DIR}:/go/src/github.com/containernetworking/plugins:z --rm golang:1.20-alpine \ /bin/sh -xe -c "\ apk --no-cache add bash tar; cd /go/src/github.com/containernetworking/plugins; umask 0022;