From 8f375fb8721a94c6e796c81aba8ce4b551a2a118 Mon Sep 17 00:00:00 2001 From: saisatishkarra Date: Thu, 23 May 2024 12:54:21 -0500 Subject: [PATCH] ci(.github): publish slsa artifacts to cloudsmith (#10215) Signed-off-by: saisatishkarra --- .github/workflows/_build_publish.yaml | 277 +++++++++++++++++++ .github/workflows/build-test-distribute.yaml | 182 ++++++++++++ mk/distribution.mk | 4 + mk/docker.mk | 6 +- 4 files changed, 467 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/_build_publish.yaml create mode 100644 .github/workflows/build-test-distribute.yaml diff --git a/.github/workflows/_build_publish.yaml b/.github/workflows/_build_publish.yaml new file mode 100644 index 000000000000..2e5fbd82bea5 --- /dev/null +++ b/.github/workflows/_build_publish.yaml @@ -0,0 +1,277 @@ +on: + workflow_call: + inputs: + FULL_MATRIX: + required: true + type: string + ALLOW_PUSH: + required: true + type: string + BINARY_ARTIFACT_NAME: + required: true + type: string + IMAGE_ARTIFACT_NAME: + required: true + type: string + IMAGES: + required: true + type: string + REGISTRY: + required: true + type: string + VERSION_NAME: + required: true + type: string + NOTARY_REPOSITORY: + required: true + type: string + outputs: + BINARY_ARTIFACT_DIGEST_BASE64: + value: ${{ jobs.build-binaries.outputs.BINARY_ARTIFACT_DIGEST_BASE64 }} + IMAGE_DIGESTS: + value: ${{ jobs.digest-images.outputs.DIGESTS }} +permissions: + contents: read + id-token: write # Required for image signing +env: + CI_TOOLS_DIR: "/home/runner/work/kuma/kuma/.ci_tools" + FULL_MATRIX: ${{ inputs.FULL_MATRIX }} + ALLOW_PUSH: ${{ inputs.ALLOW_PUSH }} + GH_OWNER: ${{ github.repository_owner }} + GH_USER: "github-actions[bot]" + GH_EMAIL: "<41898282+github-actions[bot]@users.noreply.github.com>" + GH_REPO: "charts" +jobs: + build-binaries: + timeout-minutes: 40 + runs-on: ubuntu-latest + outputs: + BINARY_ARTIFACT_DIGEST_BASE64: ${{ steps.inspect-binary-output.outputs.binary_artifact_digest_base64 }} + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: go.mod + - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: | + ${{ env.CI_TOOLS_DIR }} + key: ${{ runner.os }}-${{ runner.arch }}-devtools-${{ hashFiles('mk/dependencies/deps.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-devtools + - run: | + make build + - run: | + make -j build/distributions + - id: inspect-binary-output + run: | + for i in build/distributions/out/*.tar.gz; do echo $i; tar -tvf $i; done + echo "Artifact digest:" + cat ./build/distributions/artifact_digest_file.text + echo "binary_artifact_digest_base64=$(cat ./build/distributions/artifact_digest_file.text)" > $GITHUB_OUTPUT + - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + id: binary-artifacts + with: + name: ${{ inputs.BINARY_ARTIFACT_NAME }} + path: | + ./build/distributions/out/*.tar.gz + ./build/distributions/out/*.sha256 + !./build/distributions/out/*.tar.gz.sha256 + retention-days: ${{ github.event_name == 'pull_request' && 1 || 30 }} + - name: publish binaries + env: + PULP_USERNAME: ${{ vars.PULP_USERNAME }} + PULP_PASSWORD: ${{ secrets.PULP_PASSWORD }} + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} + run: | + make publish/pulp + build-images: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + image: ${{ fromJSON(inputs.images) }} + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + - name: Install dependencies for cross builds + if: ${{ fromJSON(inputs.FULL_MATRIX) }} + run: | + sudo apt-get update; sudo apt-get install -y qemu-user-static binfmt-support + - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: go.mod + - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: | + ${{ env.CI_TOOLS_DIR }} + key: ${{ runner.os }}-${{ runner.arch }}-devtools-${{ hashFiles('mk/dependencies/deps.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-devtools + - run: | + make dev/tools + - id: image_meta + run: | + echo "Extracting image meta for ${{ matrix.image }}" + echo "image=${{ inputs.REGISTRY }}/${{ matrix.image }}:${{ inputs.VERSION_NAME }}" >> $GITHUB_OUTPUT + - run: | + make images/${{ matrix.image }} + - run: | + make docker/save/${{ matrix.image }} + - name: Run container structure test + if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci/skip-container-structure-test') && !contains(github.event.pull_request.labels.*.name, 'ci/skip-test') }} + run: | + make test/container-structure/${{ matrix.image }} + - name: scan amd64 image + id: scan_image-amd64 + uses: Kong/public-shared-actions/security-actions/scan-docker-image@62643b74f79f6a697b9add1a2f9c069bf9ca1250 # v2.3.0 + with: + asset_prefix: image_${{ matrix.image }}-amd64 + image: ./build/docker/${{ matrix.image }}-amd64.tar + upload-sbom-release-assets: true + - name: scan arm64 image + id: scan_image-arm64 + if: ${{ fromJSON(inputs.FULL_MATRIX) }} + uses: Kong/public-shared-actions/security-actions/scan-docker-image@62643b74f79f6a697b9add1a2f9c069bf9ca1250 # v2.3.0 + with: + asset_prefix: image_${{ matrix.image }}-arm64 + image: ./build/docker/${{ matrix.image }}-arm64.tar + upload-sbom-release-assets: true + # TODO in the future we may want to have prerelease images and use `regctl image copy` to move them to their final location + - name: publish images + id: release_images + env: + DOCKER_API_KEY: ${{ secrets.DOCKER_API_KEY }} + DOCKER_USERNAME: ${{ vars.DOCKER_USERNAME }} + run: |- + make docker/login + # ensure we always logout + function on_exit() { + make docker/logout + } + trap on_exit EXIT + make docker/push/${{ matrix.image }} + make docker/manifest/${{ matrix.image }} + - name: Install regctl + uses: regclient/actions/regctl-installer@d8097ee5dd5cdf150516315919b58509fc7f4cfa + - name: image digest + id: image_digest + if: ${{ fromJSON(inputs.ALLOW_PUSH) }} + run: | + echo "Fetching image digest for ${{ matrix.image }}" + digest=$(regctl image digest ${{ steps.image_meta.outputs.image }}) + echo "Got digest: $digest" + echo "digest=${digest}" >> $GITHUB_OUTPUT + echo "{\"${{matrix.image}}\": \"${digest}\"}" > ./build/docker/${{ matrix.image }}.digest.json + - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + id: image-artifacts + with: + name: image_${{ matrix.image }} + path: | + ./build/docker/*.tar + retention-days: ${{ github.event_name == 'pull_request' && 1 || 30 }} + - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + id: image-digest-artifacts + with: + name: image_${{ matrix.image }}.digest.json + path: | + ./build/docker/${{ matrix.image }}.digest.json + retention-days: ${{ github.event_name == 'pull_request' && 1 || 30 }} + - name: sign image + if: ${{ fromJSON(inputs.ALLOW_PUSH) }} + id: sign + uses: Kong/public-shared-actions/security-actions/sign-docker-image@62643b74f79f6a697b9add1a2f9c069bf9ca1250 # v2.3.0 + with: + image_digest: ${{ steps.image_digest.outputs.digest }} + tags: ${{ steps.image_meta.outputs.image }} + signature_registry: ${{ inputs.REGISTRY }}/${{inputs.NOTARY_REPOSITORY}} + registry_username: ${{ vars.DOCKER_USERNAME }} + registry_password: ${{ secrets.DOCKER_API_KEY }} + digest-images: + needs: [build-images] + runs-on: ubuntu-latest + outputs: + DIGESTS: ${{ steps.compute-digests.outputs.digests }} + steps: + - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + pattern: "image_*.digest.json" + path: ./digests + merge-multiple: true + - id: compute-digests + run: | + # Create an object of digests indexed by image (.e.g: {"kuma-cp": "sha256:1234", "kuma-dp": "sha256:5678" ...}) + echo "digests<> $GITHUB_OUTPUT + jq --slurp 'reduce .[] as $item ({}; . * $item)' ./digests/*.digest.json >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + publish-helm: + needs: [build-images] + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + - name: Install dependencies for cross builds + if: ${{ fromJSON(inputs.FULL_MATRIX) }} + run: | + sudo apt-get update; sudo apt-get install -y qemu-user-static binfmt-support + - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: go.mod + cache-dependency-path: | + go.sum + - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: | + ${{ env.CI_TOOLS_DIR }} + key: ${{ runner.os }}-${{ runner.arch }}-devtools-${{ hashFiles('mk/dependencies/deps.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-devtools + - run: | + make dev/tools + - name: package-helm-chart + id: package-helm + env: + HELM_DEV: ${{ github.ref_type != 'tag' }} + run: | + make helm/update-version + + git config user.name "${GH_USER}" + git config user.email "${GH_EMAIL}" + git add -u deployments/charts + # This commit never ends up in the repo + git commit --allow-empty -m "ci(helm): update versions" + # To get an idea of what's in the commit to debug + git show + + make helm/package + PKG_FILENAME=$(find .cr-release-packages -type f -printf "%f\n") + echo "filename=${PKG_FILENAME}" >> $GITHUB_OUTPUT + - name: Upload packaged chart + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: ${{ steps.package-helm.outputs.filename }} + path: .cr-release-packages/${{ steps.package-helm.outputs.filename }} + retention-days: ${{ github.event_name == 'pull_request' && 1 || 30 }} + # Everything from here is only running on releases. + # Ideally we'd finish the workflow early, but this isn't possible: https://github.com/actions/runner/issues/662 + - name: Generate GitHub app token + id: github-app-token + if: ${{ github.ref_type == 'tag' }} + uses: actions/create-github-app-token@a0de6af83968303c8c955486bf9739a57d23c7f1 # v1.10.0 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ env.GH_REPO }} + - name: Release chart + if: ${{ github.ref_type == 'tag' }} + env: + GITHUB_APP: "true" + GH_TOKEN: ${{ steps.github-app-token.outputs.token }} + run: make helm/release diff --git a/.github/workflows/build-test-distribute.yaml b/.github/workflows/build-test-distribute.yaml new file mode 100644 index 000000000000..d5df364d4057 --- /dev/null +++ b/.github/workflows/build-test-distribute.yaml @@ -0,0 +1,182 @@ +name: "build-test-distribute" +on: + push: + branches: ["master", "release-*", "!*-merge-master"] + tags: ["*"] + pull_request: + branches: ["master", "release-*"] +permissions: + contents: write # To upload assets + id-token: write # For using token to sign images + actions: read # For getting workflow run info to build provenance + packages: write # Required for publishing provenance. Issue: https://github.com/slsa-framework/slsa-github-generator/tree/main/internal/builders/container#known-issues +env: + KUMA_DIR: "." + CI_TOOLS_DIR: "/home/runner/work/kuma/kuma/.ci_tools" +jobs: + check: + permissions: + contents: read + # golangci-lint-action + checks: write + timeout-minutes: 15 + runs-on: ubuntu-latest + env: + FULL_MATRIX: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'ci/run-full-matrix') }} + ALLOW_PUSH: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'ci/force-publish') }} + BUILD: ${{ github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'ci/run-build') || contains(github.event.pull_request.labels.*.name, 'ci/force-publish') }} + FORCE_PUBLISH_FROM_FORK: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci/force-publish') && github.event.pull_request.head.repo.full_name != github.repository }} + outputs: + FULL_MATRIX: ${{ env.FULL_MATRIX }} + ALLOW_PUSH: ${{ env.ALLOW_PUSH }} + BUILD: ${{ env.BUILD }} + IMAGES: ${{ steps.metadata.outputs.images }} + REGISTRY: ${{ steps.metadata.outputs.registry }} + VERSION_NAME: ${{ steps.metadata.outputs.version }} + NOTARY_REPOSITORY: ${{ (contains(steps.metadata.outputs.version, 'preview') && 'notary-internal') || 'notary' }} + CLOUDSMITH_REPOSITORY: ${{ steps.metadata.outputs.distribution_repository }} + steps: + - name: "Fail when 'ci/force-publish' label is present on PRs from forks" + if: ${{ fromJSON(env.FORCE_PUBLISH_FROM_FORK) }} + run: | + echo "::error title=Label 'ci/force-publish' cannot be used on PRs from forks::To prevent accidental exposure of secrets, CI won't use repository secrets on pull requests from forks" + exit 1 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + fetch-depth: 0 + - uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: go.mod + cache: false + - uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1 + with: + args: --fix=false --verbose + version: v1.56.1 + - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: | + ${{ env.CI_TOOLS_DIR }} + key: ${{ runner.os }}-${{ runner.arch }}-devtools-${{ hashFiles('mk/dependencies/deps.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-devtools + - run: | + make dev/tools + - run: | + make clean + - run: | + make check + - id: sca-project + uses: Kong/public-shared-actions/security-actions/sca@62643b74f79f6a697b9add1a2f9c069bf9ca1250 # v2.3.0 + with: + dir: . + config: .syft.yaml + upload-sbom-release-assets: true + - id: metadata + run: | + echo "images=$(make images/info/release/json)" >> $GITHUB_OUTPUT + echo "registry=$(make docker/info/registry)" >> $GITHUB_OUTPUT + echo "version=$(make build/info/version)" >> $GITHUB_OUTPUT + echo "distribution_repository=$(make build/info/cloudsmith_repository)" >> $GITHUB_OUTPUT + test: + permissions: + contents: read + needs: ["check"] + uses: ./.github/workflows/_test.yaml + with: + FULL_MATRIX: ${{ needs.check.outputs.FULL_MATRIX }} + secrets: inherit + build_publish: + permissions: + contents: read + id-token: write + needs: ["check", "test"] + uses: ./.github/workflows/_build_publish.yaml + if: ${{ fromJSON(needs.check.outputs.BUILD) }} + with: + FULL_MATRIX: ${{ needs.check.outputs.FULL_MATRIX }} + ALLOW_PUSH: ${{ needs.check.outputs.ALLOW_PUSH }} + IMAGE_ARTIFACT_NAME: "image_artifacts" + BINARY_ARTIFACT_NAME: "binary_artifacts" + IMAGES: ${{ needs.check.outputs.IMAGES }} + REGISTRY: ${{ needs.check.outputs.REGISTRY }} + NOTARY_REPOSITORY: ${{ needs.check.outputs.NOTARY_REPOSITORY }} + VERSION_NAME: ${{ needs.check.outputs.VERSION_NAME }} + secrets: inherit + provenance: + needs: ["check", "build_publish"] + if: ${{ github.ref_type == 'tag' }} + uses: ./.github/workflows/_provenance.yaml + secrets: inherit + permissions: + contents: write + id-token: write # For using token to sign images + actions: read # For getting workflow run info to build provenance + packages: write # Required for publishing provenance. Issue: https://github.com/slsa-framework/slsa-github-generator/tree/main/internal/builders/container#known-issues + with: + BINARY_ARTIFACTS_HASH_AS_FILE: ${{ needs.build_publish.outputs.BINARY_ARTIFACT_DIGEST_BASE64 }} + IMAGES: ${{ needs.check.outputs.IMAGES }} + REGISTRY: ${{ needs.check.outputs.REGISTRY }} + NOTARY_REPOSITORY: ${{ needs.check.outputs.NOTARY_REPOSITORY }} + IMAGE_DIGESTS: ${{ needs.build_publish.outputs.IMAGE_DIGESTS }} + distributions: + needs: ["build_publish", "check", "test", "provenance"] + timeout-minutes: 10 + if: ${{ always() }} + runs-on: ubuntu-latest + permissions: + contents: write + actions: read # For getting workflow run info + env: + SECURITY_ASSETS_DOWNLOAD_PATH: "${{ github.workspace }}/security-assets" + SECURITY_ASSETS_PACKAGE_NAME: "security-assets" # Cloudsmith package for hosting security assets + steps: + - name: "Halt due to previous failures" + run: |- + echo "results: ${{ toJson(needs.*.result) }}" + # for some reason, GH Action will always trigger a downstream job even if there are errors in an dependent job + # so we manually check it here. An example could be found here: https://github.com/kumahq/kuma/actions/runs/7044980149 + [[ ${{ contains(needs.*.result, 'failure')|| contains(needs.*.result, 'cancelled') }} == "true" ]] && exit 1 + echo "All dependent jobs succeeded" + - name: "Download all SBOM assets" + id: collect_sbom + if: ${{ needs.build_publish.result == 'success' }} + uses: actions/download-artifact@v4 + with: + path: ${{ env.SECURITY_ASSETS_DOWNLOAD_PATH }} + pattern: "*sbom.{cyclonedx,spdx}.json" + merge-multiple: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Download binary artifact provenance" + if: ${{ needs.provenance.result == 'success' && github.ref_type == 'tag' }} + id: collect_provenance + uses: actions/download-artifact@v4 + with: + path: ${{ env.SECURITY_ASSETS_DOWNLOAD_PATH }} + pattern: ${{ github.event.repository.name }}.intoto.jsonl + merge-multiple: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Generate security assets TAR" + if: ${{ needs.build_publish.result == 'success' }} + id: security_assets_metadata + run: | + cd ${{ env.SECURITY_ASSETS_DOWNLOAD_PATH }} + find . -maxdepth 1 -type f \( -name '*sbom.*.json' -o -name '*.intoto.jsonl' \) -print | tar -cvzf ${{ env.SECURITY_ASSETS_PACKAGE_NAME }}.tar.gz -T - + ls -alR . + # Publish aggregated zip file of SBOMs and/or Binary Provenance to artifact regstry + - name: Push security assets to cloudsmith + id: push_security_assets + if: ${{ needs.provenance.result == 'success' || needs.build_publish.result == 'success' }} + uses: cloudsmith-io/action@f04b4de7550751e32961ac16543116f8f5f9bfc2 # v0.6.6 + with: + api-key: ${{ secrets.CLOUDSMITH_API_KEY }} + command: "push" + format: "raw" + owner: "kong" + repo: "${{ needs.check.outputs.CLOUDSMITH_REPOSITORY }}" + version: "${{ needs.check.outputs.VERSION_NAME }}" + file: "${{ env.SECURITY_ASSETS_DOWNLOAD_PATH }}/${{ env.SECURITY_ASSETS_PACKAGE_NAME }}.tar.gz" + name: "${{ env.SECURITY_ASSETS_PACKAGE_NAME }}" + summary: "SLSA security artifacts for ${{ github.repository }}" + description: "SBOM and Binary artifact Provenance for ${{ github.repository }}" diff --git a/mk/distribution.mk b/mk/distribution.mk index e15fb1dc77eb..4cc47720c26a 100644 --- a/mk/distribution.mk +++ b/mk/distribution.mk @@ -99,6 +99,10 @@ ENABLED_DIST_NAMES=$(filter $(addprefix %,$(ENABLED_ARCH_OS)),$(foreach elt,$(DI .PHONY: build/distributions ## Build tar.gz for each enabled distribution build/distributions: $(patsubst %,build/distributions/out/$(DISTRIBUTION_TARGET_NAME)-%.tar.gz,$(ENABLED_DIST_NAMES)) +.PHONY: build/info/distribution/repo +build/info/cloudsmith_repository: + @echo $(PULP_PACKAGE_TYPE)-binaries-$(PULP_DIST_VERSION) + # Create a main target which will publish to pulp each to the tar.gz built .PHONY: publish/pulp ## Publish to pulp all enabled distributions publish/pulp: $(addprefix publish/pulp/$(DISTRIBUTION_TARGET_NAME)-,$(ENABLED_DIST_NAMES)) diff --git a/mk/docker.mk b/mk/docker.mk index b60a92c0bf06..46f3b6b74ee0 100644 --- a/mk/docker.mk +++ b/mk/docker.mk @@ -23,7 +23,8 @@ images/show: ## output all images that are built with the current configuration export DOCKER_BUILDKIT := 1 # add targets to build images for each arch -# $(1) - GOOS to build for +# $(1) - GOARCH to build for + define IMAGE_TARGETS_BY_ARCH .PHONY: image/static/$(1) image/static/$(1): ## Dev: Rebuild `kuma-static` Docker image @@ -69,8 +70,9 @@ $(foreach goarch,$(SUPPORTED_GOARCHES),$(eval $(call IMAGE_TARGETS_BY_ARCH,$(goa # add targets to generate docker/{save,load,tag,push} for each supported ARCH # add targets to build images for each arch -# $(1) - GOOS to build for +# $(1) - Image Name to build for # $(2) - GOARCH to build for +# (TODO): Support image platform in output file names define DOCKER_TARGETS_BY_ARCH .PHONY: docker/$(1)/$(2)/save docker/$(1)/$(2)/save: