From 63ef8579cdeaf256e5b3649119a15e791b63b09d Mon Sep 17 00:00:00 2001 From: Christoph Voigt Date: Sun, 28 Jan 2024 09:23:28 +0100 Subject: [PATCH] add CI from main to build new kwasm-lifefycle-manager images (#70) * add ci from main * adjust ci to build for dev branch * fix linting issues --- .github/dependabot.yml | 17 +-- .github/release-drafter.yml | 66 ++++++++++++ .github/workflows/ci.yml | 33 ++++++ .github/workflows/container-build.yml | 38 +++++++ .github/workflows/container-image.yml | 62 +++++++++++ .github/workflows/helm-chart-release.yml | 42 ++++++++ .github/workflows/release-drafter.yml | 37 +++++++ .github/workflows/release.yml | 127 +++++++++++++++++++++++ .github/workflows/sbom.yml | 85 +++++++++++++++ .github/workflows/sign-image.yml | 33 ++++++ cmd/main.go | 25 +++-- internal/controller/shim_controller.go | 5 +- 12 files changed, 549 insertions(+), 21 deletions(-) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/container-build.yml create mode 100644 .github/workflows/container-image.yml create mode 100644 .github/workflows/helm-chart-release.yml create mode 100644 .github/workflows/release-drafter.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/sbom.yml create mode 100644 .github/workflows/sign-image.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6b90713..e11a679 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,11 +1,14 @@ version: 2 updates: - - package-ecosystem: "" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "gomod" + directory: "/" schedule: interval: "daily" - versioning-strategy: "lockfile-only" - allowed_updates: - - match: - dependency_type: "all" - update_type: "semver:patch" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..2a8d19b --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,66 @@ +categories: + - title: '⚠️ Breaking changes' + labels: + - 'kind/major' + - 'kind/breaking-change' + - title: '🚀 Features' + labels: + - 'kind/enhancement' + - 'kind/feature' + - title: '🐛 Bug Fixes' + labels: + - 'kind/bug' + - title: '🧰 Maintenance' + labels: + - 'kind/chore' + - 'area/dependencies' + +exclude-labels: + - duplicate + - invalid + - later + - wontfix + - kind/question + - release/skip-changelog + +change-template: '- $TITLE (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +name-template: 'v$RESOLVED_VERSION' +template: | + $CHANGES + +autolabeler: + # Tag any PR with "!" in the subject as major update. In other words, breaking change + - label: 'kind/breaking-change' + title: '/.*!:.*/' + - label: 'area/dependencies' + title: 'chore(deps)' + - label: 'area/dependencies' + title: 'fix(deps)' + - label: 'area/dependencies' + title: 'build(deps)' + - label: 'kind/feature' + title: 'feat' + - label: 'kind/bug' + title: 'fix' + - label: 'kind/chore' + title: 'chore' + +version-resolver: + major: + labels: + - 'kind/major' + - 'kind/breaking-change' + minor: + labels: + - 'kind/minor' + - 'kind/feature' + - 'kind/enhancement' + patch: + labels: + - 'kind/patch' + - 'kind/fix' + - 'kind/bug' + - 'kind/chore' + - 'area/dependencies' + default: patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..76159c4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + workflow_call: + push: + pull_request: + +# Declare default permissions as read only. +permissions: read-all + +jobs: + unit_tests: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: "1.21" + - run: make test + + golangci: + name: Golangci-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: "1.21" + - name: golangci-lint + uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0 + with: + version: v1.54.2 diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml new file mode 100644 index 0000000..9ab002a --- /dev/null +++ b/.github/workflows/container-build.yml @@ -0,0 +1,38 @@ +name: Build container image, sign it, and generate SBOMs + +on: + workflow_call: + outputs: + digest: + description: "Container image digest" + value: ${{jobs.build.outputs.digest}} + + push: + branches: + - "kwasm-lifecycle-manager" + +jobs: + build: + uses: ./.github/workflows/container-image.yml + permissions: + packages: write + with: + push-image: true + + sign: + needs: build + uses: ./.github/workflows/sign-image.yml + permissions: + packages: write + id-token: write + with: + image-digest: ${{ needs.build.outputs.digest }} + + sbom: + needs: build + uses: ./.github/workflows/sbom.yml + permissions: + packages: write + id-token: write + with: + image-digest: ${{ needs.build.outputs.digest }} diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml new file mode 100644 index 0000000..af65820 --- /dev/null +++ b/.github/workflows/container-image.yml @@ -0,0 +1,62 @@ +name: Build container image + +on: + workflow_call: + inputs: + push-image: + type: boolean + required: true + outputs: + repository: + description: "Repository used to build the container image" + value: ${{ jobs.build.outputs.repository }} + version: + description: "Version used to build the container image" + value: ${{ jobs.build.outputs.version }} + digest: + description: "Image digest" + value: ${{ jobs.build.outputs.digest }} + +jobs: + build: + name: Build container image + permissions: + packages: write + runs-on: ubuntu-latest + outputs: + repository: ${{ steps.setoutput.outputs.repository }} + version: ${{ steps.setoutput.outputs.version }} + artifact: ${{ steps.setoutput.outputs.artifact }} + digest: ${{ steps.setoutput.outputs.digest }} + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Set up QEMU + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Setup version info + run: echo "VERSION=$(date +%Y%m%d-%H%M%S)-g$(git rev-parse --short HEAD)" >> $GITHUB_ENV + - name: Build and push container image + if: ${{ inputs.push-image }} + id: build-image + uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64, linux/arm64 + push: true + tags: | + ghcr.io/${{ env.IMG_REPOSITORY_NAME }}/kwasm-operator:${{ env.VERSION }} + - id: setoutput + name: Set output parameters + run: | + echo "repository=ghcr.io/${{ env.IMG_REPOSITORY_NAME }}/kwasm-operator" >> $GITHUB_OUTPUT + echo "version=${{ ${{ env.VERSION }} }}" >> $GITHUB_OUTPUT + echo "digest=${{ steps.build-image.outputs.digest }}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/helm-chart-release.yml b/.github/workflows/helm-chart-release.yml new file mode 100644 index 0000000..4ff8cac --- /dev/null +++ b/.github/workflows/helm-chart-release.yml @@ -0,0 +1,42 @@ +# This action releases the kwasm-operator helm chart +# The action must run on each commit done against main, however +# a new release will be performed **only** when a change occurs inside +# of the `charts` directory. +name: Release helm chart + +on: + push: + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + + permissions: + id-token: write + packages: write + contents: write + + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Install Helm + uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5 + with: + version: v3.10.0 + + - name: Run chart-releaser + if: github.ref == 'refs/heads/main' + uses: helm/chart-releaser-action@a917fd15b20e8b64b94d9158ad54cd6345335584 # v1.6.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + CR_RELEASE_NAME_TEMPLATE: "{{ .Name }}-chart-{{ .Version }}" diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..694ac00 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,37 @@ +name: Release Drafter + +on: + workflow_dispatch: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + # pull_request event is required only for autolabeler + pull_request: + # Only following types are handled by the action, but one can default to all as well + types: [opened, reopened, synchronize, edited] + # pull_request_target event is required for autolabeler to support PRs from forks + pull_request_target: + types: [opened, reopened, synchronize, edited] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5.25.0 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b1d167f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,127 @@ +name: kwasm-operator release +on: + push: + tags: + - "v*" + +# Declare default permissions as read only. +permissions: read-all + +jobs: + ci: + uses: ./.github/workflows/ci.yml + permissions: read-all + + build: + name: Build container image, sign it, and generate SBOMs + uses: ./.github/workflows/container-build.yml + permissions: + id-token: write + packages: write + + release: + name: Create release + + needs: + - ci + - build + + permissions: + contents: write + + runs-on: ubuntu-latest + + steps: + - name: Retrieve tag name + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + echo TAG_NAME=$(echo ${{ github.ref_name }}) >> $GITHUB_ENV + + - name: Get latest release tag + id: get_last_release_tag + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + let release = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + if (release.status === 200 ) { + core.setOutput('old_release_tag', release.data.tag_name) + return + } + core.setFailed("Cannot find latest release") + + - name: Get release ID from the release created by release drafter + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + let releases = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + for (const release of releases.data) { + if (release.draft) { + core.info(release) + core.exportVariable('RELEASE_ID', release.id) + return + } + } + core.setFailed(`Draft release not found`) + + - name: Download SBOM artifact + uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + with: + pattern: sbom-* + path: ./ + merge-multiple: true + + - name: Display structure of downloaded files + run: ls -R + + - name: Upload release assets + id: upload_release_assets + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + let fs = require('fs'); + let path = require('path'); + + let files = [ + 'kwasm-operator-sbom-amd64.spdx', + 'kwasm-operator-sbom-amd64.spdx.cert', + 'kwasm-operator-sbom-amd64.spdx.sig', + 'kwasm-operator-sbom-arm64.spdx', + 'kwasm-operator-sbom-arm64.spdx.cert', + 'kwasm-operator-sbom-arm64.spdx.sig'] + const {RELEASE_ID} = process.env + + for (const file of files) { + let file_data = fs.readFileSync(file); + + let response = await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: `${RELEASE_ID}`, + name: path.basename(file), + data: file_data, + }); + } + + - name: Publish release + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const {RELEASE_ID} = process.env + const {TAG_NAME} = process.env + github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: `${RELEASE_ID}`, + draft: false, + tag_name: `${TAG_NAME}`, + name: `${TAG_NAME}`, + prerelease: `${{ contains(github.event.workflow_run.head_branch, '-alpha') || contains(github.event.workflow_run.head_branch, '-beta') || contains(github.event.workflow_run.head_branch, '-rc') }}`, + make_latest: `${{ !(contains(github.event.workflow_run.head_branch, '-alpha') || contains(github.event.workflow_run.head_branch, '-beta') || contains(github.event.workflow_run.head_branch, '-rc')) }}` + }); diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..e8aa5d1 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,85 @@ +name: Generate SBOMs + +on: + workflow_call: + inputs: + image-digest: + type: string + required: true + +jobs: + sbom: + name: Generate SBOM, sign and attach them to OCI image + strategy: + matrix: + arch: [amd64, arm64] + + permissions: + packages: write + id-token: write + + runs-on: ubuntu-latest + steps: + - name: Install cosign + uses: sigstore/cosign-installer@9614fae9e5c5eddabb09f90a270fcb487c9f7149 # v3.3.0 + + - name: Install the syft command + uses: anchore/sbom-action/download-syft@719133684c7d294116626d1344fe64f0d2ff3e9e # v0.15.2 + + - name: Install the crane command + uses: IAreKyleW00t/crane-installer@51d8c3cde789d00a49bcb1aaaf55c5f55fe60674 # v1.3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Find platform digest + shell: bash + run: | + set -e + IMG_REPOSITORY_NAME=$( echo ${{ github.repository_owner }} | awk '{print tolower($0)}' ) + echo IMG_REPOSITORY_NAME=${IMG_REPOSITORY_NAME} >> $GITHUB_ENV + DIGEST=$(crane digest \ + --platform "linux/${{ matrix.arch }}" \ + ghcr.io/${IMG_REPOSITORY_NAME}/kwasm-operator@${{ inputs.image-digest }}) + echo "PLATFORM_DIGEST=${DIGEST}" >> "$GITHUB_ENV" + + - name: Create SBOM file + shell: bash + run: | + syft \ + -o spdx-json \ + --file kwasm-operator-sbom-${{ matrix.arch }}.spdx \ + ghcr.io/${{ env.IMG_REPOSITORY_NAME }}/kwasm-operator@${{ env.PLATFORM_DIGEST }} + + - name: Sign SBOM file + run: | + cosign sign-blob --yes \ + --output-certificate kwasm-operator-sbom-${{ matrix.arch }}.spdx.cert \ + --output-signature kwasm-operator-sbom-${{ matrix.arch }}.spdx.sig \ + kwasm-operator-sbom-${{ matrix.arch }}.spdx + + - name: Attach SBOM file in the container image + shell: bash + run: | + cosign attach \ + sbom --sbom kwasm-operator-sbom-${{ matrix.arch }}.spdx \ + ghcr.io/${{ env.IMG_REPOSITORY_NAME }}/kwasm-operator@${{ env.PLATFORM_DIGEST }} + + - name: Sign SBOM file pushed to OCI registry + shell: bash + run: | + set -e + SBOM_TAG="$(echo ${{ env.PLATFORM_DIGEST }} | sed -e 's/:/-/g').sbom" + + cosign sign --yes \ + ghcr.io/${{ env.IMG_REPOSITORY_NAME }}/kwasm-operator:${SBOM_TAG} + + - name: Upload SBOMs as artifacts + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + with: + name: sbom-${{ matrix.arch }} + path: kwasm-operator-sbom-* diff --git a/.github/workflows/sign-image.yml b/.github/workflows/sign-image.yml new file mode 100644 index 0000000..94ddf6d --- /dev/null +++ b/.github/workflows/sign-image.yml @@ -0,0 +1,33 @@ +name: Sign image + +on: + workflow_call: + inputs: + image-digest: + type: string + required: true + +jobs: + sign: + name: Sign image + permissions: + packages: write + id-token: write + + runs-on: ubuntu-latest + steps: + - name: Install cosign + uses: sigstore/cosign-installer@9614fae9e5c5eddabb09f90a270fcb487c9f7149 # v3.3.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Sign container image + run: | + IMG_REPOSITORY_NAME=$( echo ${{ github.repository_owner }} | awk '{print tolower($0)}' ) + cosign sign --yes \ + ghcr.io/${IMG_REPOSITORY_NAME}/kwasm-operator@${{ inputs.image-digest }} diff --git a/cmd/main.go b/cmd/main.go index d72fdc5..7e99f8d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,7 +18,6 @@ package main import ( "flag" - "fmt" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -52,18 +51,18 @@ func init() { } // getWatchNamespace returns the Namespace the operator should be watching for changes -func getWatchNamespace() string { - // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE - // which specifies the Namespace to watch. - // An empty value means the operator will fail to start. - watchNamespaceEnvVar := "CONTROLLER_NAMESPACE" - - ns, found := os.LookupEnv(watchNamespaceEnvVar) - if !found { - panic(fmt.Sprintf("env var '%s' must be set", watchNamespaceEnvVar)) - } - return ns -} +// func getWatchNamespace() string { +// // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE +// // which specifies the Namespace to watch. +// // An empty value means the operator will fail to start. +// watchNamespaceEnvVar := "CONTROLLER_NAMESPACE" + +// ns, found := os.LookupEnv(watchNamespaceEnvVar) +// if !found { +// panic(fmt.Sprintf("env var '%s' must be set", watchNamespaceEnvVar)) +// } +// return ns +// } func main() { var metricsAddr string diff --git a/internal/controller/shim_controller.go b/internal/controller/shim_controller.go index f02d364..337e904 100644 --- a/internal/controller/shim_controller.go +++ b/internal/controller/shim_controller.go @@ -119,7 +119,10 @@ func (sr *ShimReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // 4. Deploy job to each node in list if len(nodes.Items) != 0 { - sr.handleDeployJob(ctx, &shimResource, nodes, req) + _, err = sr.handleDeployJob(ctx, &shimResource, nodes, req) + if err != nil { + return ctrl.Result{}, err + } } else { log.Info().Msg("No nodes found") }