From dda90df8db52da028fe8a7bb63da0b5ec95aa41b Mon Sep 17 00:00:00 2001 From: Christian Cwienk Date: Mon, 24 Feb 2025 10:08:42 +0100 Subject: [PATCH 1/6] add oci-ocm-multiarch workflow With the advent of arm64-runners, it makes sense to have a convenient reusable workflow for building multiarch OCI-Images (grouped in single OCI Indexes), delegating image-builds to native runners (to avoid overhead from emulation). As runners can only be specified on job-level, this cannot be achieved using an action (as done w/ ocm-oci-build action), hence use a reusable workflow. Unfortunately, workflows are less flexible w.r.t. customations, which results in a wider set of input-parameters. --- .github/workflows/oci-ocm-multiarch.yaml | 301 +++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 .github/workflows/oci-ocm-multiarch.yaml diff --git a/.github/workflows/oci-ocm-multiarch.yaml b/.github/workflows/oci-ocm-multiarch.yaml new file mode 100644 index 000000000..5a588cedf --- /dev/null +++ b/.github/workflows/oci-ocm-multiarch.yaml @@ -0,0 +1,301 @@ +name: OCI-Build (multiarch) +description: | + Reusable Workflow for building multi-platform OCI Images (grouped with an OCI Image Index), + accompanied by an OCM Component Descriptor Resource fragment. + + The workflow will spawn a separate job for each specified platform, trying to use matching runners. + Currently, native runners for either `linux/arm64`. and `linux/amd64` are used. Other platforms + will be built on runners on `linux/amd64`, using emulation. +on: + workflow_call: + inputs: + oci-registry: + description: | + The (optional) oci-registry as part of image-push-target. + If both oci-registry, and oci-repository are present, push-target is calculated as: + {oci-registry}/{oci-repository} + type: string + required: false + oci-repository: + description: | + The oci-repository as part of image-push-target. + If both oci-registry, and oci-repository are present, push-target is calculated as: + {oci-registry}/{oci-repository} + If only one of both is passed, the missing parameter will be ignored. + type: string + required: true + oci-platforms: + description: | + The (comma-separated) OCI Platforms to build for. Only linux/arm64 and linux/amd64 are + currently supported for being run natively on arm/x86_64-runners respectively. + type: string + default: 'linux/arm64,linux/amd64' + tag: + description: | + The image-tag to set. Occurrences of `{version}` will be replaced by `version` input + (which is the default). + type: string + default: '{version}' + required: false + target: + description: | + The (optional) target to build (passed to `docker build` as --target). + type: string + required: false + dockerfile: + type: string + required: false + build-ctx-artefact: + description: | + If passed, this value is interpreted as an GitHub Actions Artifact name. It is downloaded + into the default working directory after doing a repository-checkout. + + Note that thus-imported files will overwrite existing ones from repository-checkout. + Contained files will be merged recursively into existing directories. + type: string + required: false + untar-build-ctx-artefact: + description: | + If passed, along w/ build-ctx-artefact, the downloaded artefact is assumed to contain + a tarfile, and will be extracted. This parameter's name is used to specify the + filename of said tarfile. This is useful, as GitHubActions choose to use ZIP by default, + so wrapping in TAR is a recommended practise, e.g. to preserve executable bits. + type: string + required: false + name: + description: | + The Image's name (used as OCM-Resource-Name) + type: string + required: true + version: + description: | + The Image's version (used as OCM-Resource-Version). Will by default also used as `tag`. + Must be a valid "relaxed semver". + type: string + required: true + ocm-labels: + description: | + optional OCM-Labels to add to the OCM Resource fragment. May either be a single label, + or a list of labels in either YAML or JSON format. + + Example (single label): + ``` + name: my-label + value: my-label-value + ``` + Example (list of labels): + ``` + - name: first-label + value: value1 + - name: second-label + value: value2 + ``` + type: string + + outputs: + ocm-resource: + description: | + OCM Resource fragment describing the published image. + value: ${{ jobs.collect-images.outputs.ocm-resource }} + +jobs: + preprocess: + runs-on: ubuntu-latest + outputs: + args: ${{ steps.preprocess.outputs.args }} + target-image-ref: ${{ steps.preprocess.outputs.target-image-ref }} + steps: + - name: preprocess + shell: python + id: preprocess + run: | + import json + import os + import pprint + + platform_names = '${{ inputs.oci-platforms }}'.split(',') + ref_parts = [] + if (oci_registry := '${{ inputs.oci-registry }}'): + ref_parts.append(oci_registry.strip('/')) + if (oci_repository := '${{ inputs.oci-repository }}'): + ref_parts.append(oci_repository.strip('/')) + + image_ref_base = '/'.join(ref_parts) + + # todo: validate version + version = '${{ inputs.version }}' + + # note: `version` must be in scope to resolve occurrences of `{version}` + tag = f'${{ inputs.tag }}' + + runners_by_platform = { + 'linux/arm64': 'ubuntu-24.04-arm', + 'linux/amd64': 'ubuntu-latest', + } + + args = [] + + for platform_name in platform_names: + if not platform_name in runners_by_platform: + print(f'WARNING: no runner for {platform_name=}') + platform_tag = f'${{ inputs.tag }}-{platform_name.replace("/", "_")}' + args.append({ + 'platform-name': platform_name, + 'runner': runners_by_platform.get(platform_name, 'ubuntu-latest'), + 'tag': platform_tag, + 'image-reference': f'{image_ref_base}:{platform_tag}', + }) + + pprint.pprint(args) + + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'args={json.dumps(args)}') + f.write('\n') + + f.write(f'target-image-ref={image_ref_base}:${{ inputs.tag }}') + f.write('\n') + + build-images: + needs: preprocess + runs-on: ${{ matrix.args.runner }} + permissions: + contents: read + packages: write + id-token: write + strategy: + matrix: + args: ${{ fromJSON(needs.preprocess.outputs.args) }} + steps: + - uses: docker/setup-buildx-action@v3 + - uses: gardener/cc-utils/.github/actions/oci-auth@master + with: + oci-image-reference: ${{ matrix.args.image-reference }} + gh-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - name: retrieve build-ctx-artefact + if: ${{ inputs.build-ctx-artefact }} + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.build-ctx-artefact }} + merge-multiple: true + - name: untar build-ctx-artefact + if: ${{ inputs.build-ctx-artefact && inputs.untar-build-ctx-artefact }} + run: | + fname="${{ inputs.untar-build-ctx-artefact }}" + echo "untarring ${fname}" + tar xf "${fname}" + - name: 'build ${{ inputs.name }} / ${{ matrix.args.platform-name }}' + uses: docker/build-push-action@v6 + id: build + with: + push: true + tags: ${{ matrix.args.image-reference }} + context: '.' + target: ${{ inputs.target }} + file: ${{ inputs.dockerfile }} + + collect-images: + needs: + - build-images + - preprocess + outputs: + ocm-resource: ${{ steps.collect-images.outputs.ocm-resource }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + steps: + - uses: gardener/cc-utils/.github/actions/install-gardener-gha-libs@master + - uses: gardener/cc-utils/.github/actions/oci-auth@master + with: + oci-image-reference: ${{ needs.preprocess.outputs.target-image-ref }} + gh-token: ${{ secrets.GITHUB_TOKEN }} + - name: collect-images + shell: python + run: | + import dataclasses + import json + import os + import pprint + import textwrap + + import yaml + + import oci.auth + import oci.client + import oci.merge + import oci.model + import ocm + + args = json.loads(''' + ${{ needs.preprocess.outputs.args }} + ''') + pprint.pprint(args) + + src_image_refs = [ + a['image-reference'] for a in args + ] + + tgt_image_ref = '${{ needs.preprocess.outputs.target-image-ref }}' + + oci_client = oci.client.Client( + credentials_lookup=oci.auth.docker_credentials_lookup(), + ) + + print(f'merging {src_image_refs=} into {tgt_image_ref=}') + oci.merge.into_image_index( + src_image_refs=src_image_refs, + tgt_image_ref=tgt_image_ref, + oci_client=oci_client, + ) + + print(f'published to {tgt_image_ref=}') + + labels_str = '''${{ inputs.ocm-labels }}''' + if labels_str: + labels = yaml.safe_load(labels_str) + if isinstance(labels, dict): + labels = [labels] + elif isinstance(labels, list): + pass + else: + raise ValueError(f'label must either be a dict or a list - saw: {type(labels)=}') + else: + labels = [] + + resource = ocm.Resource( + name='${{ inputs.name }}', + version='${{ inputs.version }}', + type=ocm.ArtefactType.OCI_IMAGE, + access=ocm.OciAccess( + imageReference=tgt_image_ref, + ), + relation=ocm.ResourceRelation.LOCAL, + labels=labels, + ) + + resource_str = yaml.dump( + data=dataclasses.asdict(resource), + Dumper=ocm.EnumValueYamlDumper, + ) + + print('ocm-resource:') + print(resource_str) + + with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write('ocm-resource< Date: Thu, 27 Feb 2025 14:52:08 +0100 Subject: [PATCH 2/6] add prebuild-hook --- .github/workflows/oci-ocm-multiarch.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/oci-ocm-multiarch.yaml b/.github/workflows/oci-ocm-multiarch.yaml index 5a588cedf..130226ff8 100644 --- a/.github/workflows/oci-ocm-multiarch.yaml +++ b/.github/workflows/oci-ocm-multiarch.yaml @@ -45,6 +45,17 @@ on: dockerfile: type: string required: false + prebuild-hook: + description: | + if set, value will be executed as a bash-expression _after_ repository-checkout and + (if configured) artefact-import, but _before_ running build. + + This can be useful, e.g. to conveniently pass-in some dynamic values from other build-jobs, + or to run some pre-generation steps. + + The value will _not_ be quoted, so any valid bash-script can be passed. + type: string + required: false build-ctx-artefact: description: | If passed, this value is interpreted as an GitHub Actions Artifact name. It is downloaded @@ -184,6 +195,10 @@ jobs: fname="${{ inputs.untar-build-ctx-artefact }}" echo "untarring ${fname}" tar xf "${fname}" + - name: prebuild-hook + if: ${{ inputs.prebuild-hook }} + run: | + ${{ inputs.prebuild-hook }} - name: 'build ${{ inputs.name }} / ${{ matrix.args.platform-name }}' uses: docker/build-push-action@v6 id: build From a96a1373d5eb5842b71321fe06915047fa8509dc Mon Sep 17 00:00:00 2001 From: Christian Cwienk Date: Thu, 27 Feb 2025 16:49:13 +0100 Subject: [PATCH 3/6] expose oci-reference as output --- .github/workflows/oci-ocm-multiarch.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/oci-ocm-multiarch.yaml b/.github/workflows/oci-ocm-multiarch.yaml index 130226ff8..935f6ba5c 100644 --- a/.github/workflows/oci-ocm-multiarch.yaml +++ b/.github/workflows/oci-ocm-multiarch.yaml @@ -108,6 +108,10 @@ on: description: | OCM Resource fragment describing the published image. value: ${{ jobs.collect-images.outputs.ocm-resource }} + oci-reference: + description: | + The OCI-Image-Reference to which the built Image-Index was pushed to + value: ${{ jobs.collect-images.outputs.oci-reference }} jobs: preprocess: @@ -215,6 +219,7 @@ jobs: - preprocess outputs: ocm-resource: ${{ steps.collect-images.outputs.ocm-resource }} + oci-reference: ${{ steps.collect-images.outputs.oci-reference }} runs-on: ubuntu-latest permissions: contents: read @@ -227,6 +232,7 @@ jobs: oci-image-reference: ${{ needs.preprocess.outputs.target-image-ref }} gh-token: ${{ secrets.GITHUB_TOKEN }} - name: collect-images + id: collect-images shell: python run: | import dataclasses @@ -299,6 +305,7 @@ jobs: print(resource_str) with open(os.environ['GITHUB_OUTPUT'], 'a') as f: + f.write(f'oci-reference={tgt_image_ref}\n') f.write('ocm-resource< Date: Thu, 27 Feb 2025 15:55:11 +0100 Subject: [PATCH 4/6] re-use oci-ocm-workflow --- .github/workflows/build-and-test.yaml | 82 +++++++++------------------ 1 file changed, 28 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 36014a569..01438d63c 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -352,7 +352,7 @@ jobs: --file component-descriptor.yaml echo "adding image-resource" - echo "${{ needs.images.outputs.ocm_resources }} " | \ + echo "${{ needs.images.outputs.ocm-resource }} " | \ python3 -m ocm append resource \ --file component-descriptor.yaml @@ -471,65 +471,39 @@ jobs: .ci/test images: - name: Build OCI Images + name: Build Job-Image needs: - version - - package + - package # needed indirectly (distribution-packages-artefact) - params - outputs: - oci_image_ref: ${{ steps.image-build.outputs.image_reference }} - ocm_resources: ${{ steps.image-build.outputs.ocm_resource }} - runs-on: ubuntu-latest - environment: build permissions: contents: read packages: write id-token: write - steps: - - uses: actions/checkout@v4 - - name: Retrieve Distribution Packages - uses: actions/download-artifact@v4 - with: - name: distribution-packages - path: /tmp/dist - - - name: prepare build-filesystem - id: prepare - run: | - cp -r /tmp/dist . - ls -lta - - setuptools_version=${{ needs.version.outputs.setuptools-version }} - # workaround: set repository-version to setuptools-version so installation of - # packages will succeed - echo "${setuptools_version}" | .ci/write-version - - name: Authenticate against OCI-Registry - uses: ./.github/actions/oci-auth - with: - gh-token: ${{ secrets.GITHUB_TOKEN }} - oci-image-reference: ${{ needs.params.outputs.oci_repository }} - - name: Build OCI Image (using ocm-oci-build-action) - uses: ./.github/actions/ocm-oci-build - id: image-build - with: - name: job-image - repository: ${{ needs.params.outputs.oci_repository }}/cicd/job-image - version: ${{ needs.version.outputs.version }} - oci_platforms: ${{ needs.params.outputs.oci_platforms }} - context: . # pass modified path rather than clean checkout - ocm_labels: | - - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions - value: - - name: containerd - version: v1.6.15 - - name: gardener.cloud/cve-categorisation - value: - authentication_enforced: true - availability_requirement: low - confidentiality_requirement: high - integrity_requirement: high - network_exposure: protected - user_interaction: gardener-operator + uses: ./.github/workflows/oci-ocm-multiarch.yaml + with: + name: job-image + oci-registry: ${{ needs.params.outputs.oci_repository }} + oci-repository: cicd/job-image + oci-platforms: ${{ needs.params.outputs.oci_platforms }} + version: ${{ needs.version.outputs.version }} + # workaround: we need to use a deviating version for setuptools + prebuild-hook: | + echo "${{ needs.version.outputs.setuptools-version }}" | .ci/write-version + build-ctx-artefact: distribution-packages + ocm-labels: | + - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions + value: + - name: containerd + version: v1.6.15 + - name: gardener.cloud/cve-categorisation + value: + authentication_enforced: true + availability_requirement: low + confidentiality_requirement: high + integrity_requirement: high + network_exposure: protected + user_interaction: gardener-operator documentation: name: Generate Documentation @@ -545,7 +519,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Generate Documentation run: | - image_ref=${{ needs.images.outputs.oci_image_ref }} + image_ref=${{ needs.images.outputs.oci-reference }} mkdir documentation-out.d docker run -v$PWD:/src \ -e GH_PAGES_PATH=/src/documentation-out.d \ From 8e97777204986ef41397c598bd5fe065141e8eb7 Mon Sep 17 00:00:00 2001 From: Christian Cwienk Date: Thu, 27 Feb 2025 17:07:28 +0100 Subject: [PATCH 5/6] tar distribution-packages avoid broken semantics of upload/download-artefacts-actions --- .github/workflows/build-and-test.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 01438d63c..8a1420f25 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -220,10 +220,11 @@ jobs: cp "${resources_file}" dist/ocm_resources.yaml echo "ocm_resources=dist/ocm_resources.yaml" >> "${GITHUB_OUTPUT}" find "${pkg_dir}" + tar czf distribution-packages.tar.gz dist - uses: actions/upload-artifact@v4 with: name: distribution-packages - path: dist/ + path: distribution-packages.tar.gz base-component-descriptor: name: Generate Base-OCM-Component-Descriptor @@ -289,7 +290,6 @@ jobs: uses: actions/download-artifact@v4 with: name: distribution-packages - path: /tmp/dist - name: Retrieve Linting Logs uses: actions/download-artifact@v4 with: @@ -307,6 +307,9 @@ jobs: set -eu version=${{ needs.version.outputs.version }} ocm_repo=${{ needs.params.outputs.ocm_repository }} + + tar xf distribution-packages.tar.gz -C /tmp + echo "importing base-component-descriptor" echo "${{ needs.base-component-descriptor.outputs.component-descriptor }}" \ > component-descriptor.yaml @@ -379,7 +382,6 @@ jobs: uses: actions/download-artifact@v4 with: name: distribution-packages - path: /tmp/dist - name: lint run: | # debug @@ -392,6 +394,7 @@ jobs: cat /tmp/apk.log exit 1 fi + tar xf distribution-packages.tar.gz -C /tmp echo "installing linters" export CFLAGS='-Wno-int-conversion' if ! pip3 install --upgrade --break-system-packages \ @@ -440,13 +443,13 @@ jobs: uses: actions/download-artifact@v4 with: name: distribution-packages - path: /tmp/dist - name: run-tests run: | set -eu echo "install dependencies for python-packages" apk add --no-cache $(cat gardener-cicd-libs.apk-packages) echo "install packages" + tar xf distribution-packages.tar.gz -C /tmp export CFLAGS='-Wno-int-conversion' if ! pip3 install --break-system-packages \ --find-links /tmp/dist \ @@ -491,6 +494,7 @@ jobs: prebuild-hook: | echo "${{ needs.version.outputs.setuptools-version }}" | .ci/write-version build-ctx-artefact: distribution-packages + untar-build-ctx-artefact: distribution-packages.tar.gz ocm-labels: | - name: cloud.gardener.cnudie/dso/scanning-hints/package-versions value: From f9fbeb8049f9b572a1da64e4d5c9cf25d7dedc24 Mon Sep 17 00:00:00 2001 From: Christian Cwienk Date: Tue, 4 Mar 2025 22:25:01 +0100 Subject: [PATCH 6/6] refactor: consistently name oci-registry parameter --- .github/workflows/build-and-test.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index 8a1420f25..852162f14 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -45,7 +45,7 @@ jobs: outputs: ocm_repository: ${{ steps.params.outputs.ocm_repository }} ocm_releases_repository: ${{ steps.params.outputs.ocm_releases_repository }} - oci_repository: ${{ steps.params.outputs.oci_repository }} + oci-registry: ${{ steps.params.outputs.oci-registry }} oci_platforms: ${{ steps.params.outputs.oci_platforms }} steps: - uses: actions/checkout@v4 @@ -79,19 +79,19 @@ jobs: if ${{ inputs.release || false }}; then ocm_repository=${releases_repo} - oci_repository=${releases_repo} + oci_registry=${releases_repo} else ocm_repository=${snapshots_repo} - oci_repository=${snapshots_repo} + oci_registry=${snapshots_repo} fi echo "ocm_releases_repository=${releases_repo}" echo "ocm_repository=${ocm_repository}" - echo "oci_repository=${oci_repository}" + echo "oci-registry=${oci_registry}" echo "ocm_releases_repository=${releases_repo}" >> "${GITHUB_OUTPUT}" echo "ocm_repository=${ocm_repository}" >> "${GITHUB_OUTPUT}" - echo "oci_repository=${oci_repository}" >> "${GITHUB_OUTPUT}" + echo "oci-registry=${oci_registry}" >> "${GITHUB_OUTPUT}" echo "oci_platforms=linux/amd64,linux/arm64" >> "${GITHUB_OUTPUT}" version: @@ -486,7 +486,7 @@ jobs: uses: ./.github/workflows/oci-ocm-multiarch.yaml with: name: job-image - oci-registry: ${{ needs.params.outputs.oci_repository }} + oci-registry: ${{ needs.params.outputs.oci-registry }} oci-repository: cicd/job-image oci-platforms: ${{ needs.params.outputs.oci_platforms }} version: ${{ needs.version.outputs.version }}