diff --git a/actions/docker-build-push-image/README.md b/actions/docker-build-push-image/README.md new file mode 100644 index 000000000..f6b4b61bd --- /dev/null +++ b/actions/docker-build-push-image/README.md @@ -0,0 +1,138 @@ +# docker-build-push-image + +This is a composite GitHub Action, used to build and push docker images to private Grafana registries. +It builds registry URLs for Grafana's registries, authenticates to them, and then +uses [docker/build-push-action] to build and push the image(s). + +This action can work 1 of 2 ways: + +1. It can be run on a single runner, and if multiple `platforms` are configured then buildx/QEMU emulation is used. +2. It can be used in conjunction with [docker-export-digest] and [docker-import-digests-push-manifest] to push untagged + images whose digests are later exported and merged into a tagged docker manifest. For true multi-arch builds. + +This can push to the following registries: + +1. Google Artifact Registry +2. DockerHub + +[docker/build-push-action]: https://github.com/docker/build-push-action +[docker-build-push-image]: ../docker-build-push-image/README.md +[docker-export-digest]: ../docker-export-digest/README.md +[docker-import-digests-push-manifest]: ../docker-import-digests-push-manifest/README.md + + + +```yaml +name: Build a Docker Image + +on: + push: + branches: + - main + +jobs: + build-push-image: + permissions: + contents: read + id-token: write + steps: + - uses: grafana/shared-workflows/actions/docker-build-push-image@docker-build-push-image/v0.0.0 + with: + platforms: linux/arm64,linux/amd64 + tags: | + ${{ github.sha }} + main + push: true + registries: "gar,dockerhub" +``` + + + +## Inputs + +| Name | Type | Description | +| ----------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `build-args` | String | List of arguments necessary for the Docker image to be built. Passed to `docker/build-push-action`. | +| `build-contexts` | String | List of additional build contexts (e.g., name=path). Passed to `docker/build-push-action`. | +| `buildkitd-config` | String | The buildkitd config file to use. Defaults to `/etc/buildkitd.toml` if you're using Grafana's self-hosted runners. Passed to `docker/setup-buildx-action`. | +| `buildkitd-config-inline` | String | The buildkitd inline config to use. Passed to `docker/setup-buildx-action`. | +| `cache-from` | String | Where cache should be fetched from. Passed to `docker/build-push-action`. | +| `cache-to` | String | Where cache should be stored to. Passed to `docker/build-push-action`. | +| `context` | String | Path to the Docker build context. Passed to `docker/build-push-action`. | +| `docker-buildx-driver` | String | The driver to use for Docker Buildx. Passed to `docker/setup-buildx-action`. | +| `dockerhub-registry` | String | DockerHub Registry to store docker images in. | +| `dockerhub-repository` | String | DockerHub Repository to store docker images in. Default: github.repository | +| `file` | String | The dockerfile to use. Passed to `docker/build-push-action`. | +| `gar-delete-credentials-file` | Boolean | Delete the Google credentials file after the action is finished. If you want to keep the credentials file for a later step, set this to false. | +| `gar-environment` | String | Environment for pushing artifacts (can be either dev or prod). This sets the GAR Project (gar-project) to either `grafanalabs-dev` or `grafanalabs-global`. | +| `gar-image` | String | Name of the image to build. Default: `${GitHub Repo Name}`. | +| `gar-registry` | String | Google Artifact Registry to store docker images in. | +| `gar-repository` | String | Override the 'repo_name' used to construct the GAR repository name. Only necessary when the GAR includes a repo name that doesn't match the GitHub repo name. Default: `docker-${GitHub Repo Name}-${gar-environment}` | +| `include-tags-in-push` | Boolean | Disables the pushing of tags, and instead includes just a list of images as docker tags. Used when pushing docker digests instead of docker tags. | +| `labels` | String | List of custom labels to add to the image as metadata (passed to `docker/build-push-action`). Passed to `docker/build-push-action`. | +| `load` | Boolean | Whether to load the built image into the local docker daemon (passed to `docker/build-push-action`). Passed to `docker/build-push-action`. | +| `outputs` | String | List of docker output destinations. Passed to `docker/build-push-action`. | +| `platforms` | String | List of platforms to build the image for. Passed to `docker/build-push-action`. | +| `push` | String | Whether to push the image to the configured registries. Passed to `docker/build-push-action`. | +| `registries` | String | CSV list of registries to build images for. Accepted registries are "gar" and "dockerhub". | +| `secrets` | String | Secrets to expose to the build. Only needed when authenticating to private repositories outside the repository in which the image is being built. Passed to `docker/build-push-action`. | +| `ssh` | String | List of SSH agent socket or keys to expose to the build Passed to `docker/build-push-action`. | +| `tags` | String | List of Docker tags to be pushed. Passed to `docker/build-push-action`. | +| `target` | String | Sets the target stage to build. Passed to `docker/build-push-action`. | + +## Outputs + +| Name | Type | Description | +| -------------- | ------ | ------------------------------------------------------------ | +| `annotations` | String | Generated annotations (from docker/metadata-action) | +| `digest` | String | Image digest (from docker/build-push-action) | +| `imageid` | String | Image ID (from docker/build-push-action) | +| `images` | String | Comma separated list of the images that were built | +| `json` | String | JSON output of tags and labels (from docker/metadata-action) | +| `labels` | String | Generated Docker labels (from docker/metadata-action) | +| `metadata` | String | Build result metadata (from docker/build-push-action) | +| `metadatajson` | String | Metadata JSON (from docker/metadata) | +| `tags` | String | Generated Docker tags (from docker/metadata-action) | +| `version` | String | Generated Docker image version (from docker/metadata-action) | + +## How we construct Google Artifact Registry Images + +The full GAR image is constructed as follows, where `gar-project` is determined by `inputs.gar-environment`. + +"${{ inputs.gar-registry }}/${{ gar-project }}/${{ inputs.gar-repository }}/${{ inputs.gar-image }}" + +## How we construct DockerHub Images + +The full DockerHub image is constructed as follows: + +"${{ inputs.dockerhub-registry }}/${{ inputs.dockerhub-repository }}" + +## Adding New Registries + +Each registry is setup as follows: + +- All inputs for a registry share the same prefix (ex: `gar-image`, `gar-repository`). +- Inputs that are used for a specific registry are _not_ required by the workflow. Instead, validation is done in a step + specific to that registry. +- To calculate which registries have been configured, we loop through `inputs.registries`, and for each registry + configured we set the outputs `include-`. Those flags can be used to create steps that only execute when X + registry is configured. +- Each registry has a Setup step. This step takes the inputs specific to that registry and generates an untagged, + `image` name for that specific registry. +- The `setup-vars` step then loops through each configured image and creates a full list of images to push. +- That's it! That list of images to push is fed to `docker/build-push-action` along with the configured tags, and each + tagged image is pushed to each registry. + +So then the full checklist of work to do to implement a new registry is: + +- [ ] Add (and document) any inputs that you need to capture. Use the same prefix for all inputs, and all inputs must + _not_ be required. +- [ ] Add a step before `setup-vars` that takes those input values and constructs a valid untagged image name for the + registry you'll be pushing to. Then set that as an output. + Ex: `echo "image=${DOCKERHUB_REGISTRY}/${DOCKERHUB_IMAGE}" | tee -a "${GITHUB_OUTPUT}"` +- [ ] Add your image into the `setup-vars` step by passing the output image into an env variable, and adding it to the + list of images to be parsed. Use the existing repos as examples. +- [ ] Add a login step that depends + on `${{ inputs.push == 'true' && steps.registries.outputs.include- == 'true' }}`, where yourRegistry is + the value that will be passed into the `registries` input. Again, use existing repos as examples. +- [ ] Celebrate diff --git a/actions/docker-build-push-image/action.yaml b/actions/docker-build-push-image/action.yaml new file mode 100644 index 000000000..7acb71c3c --- /dev/null +++ b/actions/docker-build-push-image/action.yaml @@ -0,0 +1,367 @@ +name: Build and Push Docker Image +description: Composite action to push a docker image to GAR or DockerHub + +inputs: + build-args: + description: | + List of arguments necessary for the Docker image to be built. + Passed to `docker/build-push-action`. + build-contexts: + description: | + List of additional build contexts (e.g., name=path). + Passed to `docker/build-push-action`. + buildkitd-config: + description: | + The buildkitd config file to use. Defaults to `/etc/buildkitd.toml` if you're using + Grafana's self-hosted runners. + Passed to `docker/setup-buildx-action`. + buildkitd-config-inline: + description: | + The buildkitd inline config to use. + Passed to `docker/setup-buildx-action`. + cache-from: + description: | + Where cache should be fetched from. + Passed to `docker/build-push-action`. + default: type=gha + cache-to: + description: | + Where cache should be stored to. + Passed to `docker/build-push-action`. + default: type=gha,mode=max + context: + description: | + Path to the Docker build context. + Passed to `docker/build-push-action`. + default: . + docker-buildx-driver: + description: | + The driver to use for Docker Buildx. + Passed to `docker/setup-buildx-action`. + default: docker-container + dockerhub-registry: + description: | + DockerHub Registry to store docker images in. + default: "docker.io" + dockerhub-repository: + description: | + DockerHub Repository to store docker images in. + Default: github.repository + default: ${{ github.repository }} + file: + description: | + The dockerfile to use. + Passed to `docker/build-push-action`. + gar-delete-credentials-file: + description: | + Delete the Google credentials file after the action is finished. + If you want to keep the credentials file for a later step, set this to false. + default: "true" + gar-environment: + description: | + Environment for pushing artifacts (can be either dev or prod). + This sets the GAR Project (gar-project) to either `grafanalabs-dev` or `grafanalabs-global`. + default: dev + gar-image: + description: | + Name of the image to build. + Default: `${GitHub Repo Name}`. + gar-registry: + description: | + Google Artifact Registry to store docker images in. + default: "us-docker.pkg.dev" + gar-repository: + description: | + Override the 'repo_name' used to construct the GAR repository name. + Only necessary when the GAR includes a repo name that doesn't match the GitHub repo name. + Default: `docker-${GitHub Repo Name}-${gar-environment}` + include-tags-in-push: + description: | + Disables the pushing of tags, and instead includes just a list of images as docker tags. + Used when pushing docker digests instead of docker tags. + default: "true" + labels: + description: | + List of custom labels to add to the image as metadata (passed to `docker/build-push-action`). + Passed to `docker/build-push-action`. + load: + description: | + Whether to load the built image into the local docker daemon (passed to `docker/build-push-action`). + Passed to `docker/build-push-action`. + default: "false" + outputs: + description: | + List of docker output destinations. + Passed to `docker/build-push-action`. + platforms: + description: | + List of platforms to build the image for. + Passed to `docker/build-push-action`. + push: + description: | + Whether to push the image to the configured registries. + Passed to `docker/build-push-action`. + registries: + description: | + CSV list of registries to build images for. + Accepted registries are "gar" and "dockerhub". + secrets: + description: | + Secrets to expose to the build. Only needed when authenticating to private repositories outside the repository in which the image is being built. + Passed to `docker/build-push-action`. + ssh: + description: | + List of SSH agent socket or keys to expose to the build + Passed to `docker/build-push-action`. + tags: + description: | + List of Docker tags to be pushed. + Passed to `docker/build-push-action`. + required: true + target: + description: | + Sets the target stage to build. + Passed to `docker/build-push-action`. + +outputs: + annotations: + description: "Generated annotations (from docker/metadata-action)" + value: ${{ steps.meta.outputs.annotations }} + digest: + description: "Image digest (from docker/build-push-action)" + value: ${{ steps.build.outputs.digest }} + imageid: + description: "Image ID (from docker/build-push-action)" + value: ${{ steps.build.outputs.imageid }} + images: + description: "Comma separated list of the images that were built" + value: ${{ steps.setup-vars.outputs.images }} + json: + description: "JSON output of tags and labels (from docker/metadata-action)" + value: ${{ steps.meta.outputs.json }} + labels: + description: "Generated Docker labels (from docker/metadata-action)" + value: ${{ steps.meta.outputs.labels }} + metadata: + description: "Build result metadata (from docker/build-push-action)" + value: ${{ steps.build.outputs.metadata }} + metadatajson: + description: "Metadata JSON (from docker/metadata)" + value: ${{ steps.meta.outputs.json }}s + tags: + description: "Generated Docker tags (from docker/metadata-action)" + value: ${{ steps.meta.outputs.tags }} + version: + description: "Generated Docker image version (from docker/metadata-action)" + value: ${{ steps.meta.outputs.version }} + +runs: + using: composite + steps: + - name: Checkout shared-workflows + env: + action_repo: ${{ github.action_repository }} + action_ref: ${{ github.action_ref }} + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: ${{ env.action_repo }} + ref: ${{ env.action_ref }} + path: _shared-workflows-docker-build-push-image + persist-credentials: false + + - name: Set registries + id: registries + shell: bash + env: + REGISTRIES: ${{ inputs.registries }} + run: | + ############################################################# + # This splits REGISTRIES on commas, and then exports + # include-=true for each registry in the list + ############################################################# + + for r in ${REGISTRIES//,/ }; do + echo "include-${r}=true" | tee -a "${GITHUB_OUTPUT}" + done + + - name: Setup GAR variables + id: setup-gar-vars + if: ${{ steps.registries.outputs.include-gar == 'true' }} + shell: bash + env: + GAR_REPO: ${{ inputs.gar-repository }} + ENVIRONMENT: ${{ inputs.gar-environment }} + GAR_IMAGE: ${{ inputs.gar-image }} + REGISTRY: ${{ inputs.gar-registry }} + GH_REPO: ${{ github.repository }} + run: | + chmod +x ./_shared-workflows-docker-build-push-image/actions/docker-build-push-image/gar-setup-project-vars.sh + ./_shared-workflows-docker-build-push-image/actions/docker-build-push-image/gar-setup-project-vars.sh + + - name: Setup DockerHub variables + if: ${{ steps.registries.outputs.include-dockerhub == 'true' }} + id: setup-dockerhub-vars + shell: bash + env: + DOCKERHUB_IMAGE: ${{ inputs.dockerhub-repository }} + DOCKERHUB_REGISTRY: ${{ inputs.dockerhub-registry }} + run: | + chmod +x ./_shared-workflows-docker-build-push-image/actions/docker-build-push-image/dockerhub-setup-project-vars.sh + ./_shared-workflows-docker-build-push-image/actions/docker-build-push-image/dockerhub-setup-project-vars.sh + + - name: Finalize build vars + id: setup-vars + shell: bash + env: + GAR_IMAGE: ${{ steps.setup-gar-vars.outputs.image }} + DOCKERHUB_IMAGE: ${{ steps.setup-dockerhub-vars.outputs.image }} + PUSH: ${{ inputs.push }} + run: | + ############################################################# + # This constructs a CSV list of images from the previous + # setup steps, and outputs that list + # + # If there are no images then we set images="dry-run-image" + # and log a warning that no images were configured + # + # If push is not true, then we log a warning that no images will be pushed + ############################################################# + + images="" + for image in "${GAR_IMAGE}" "${DOCKERHUB_IMAGE}" + do + # if an image is not the emptry string, add it to the list + if [ -n "${image}" ]; then + + # if we already have any images, prefix a comma + if [ -n "${images}" ]; then + images="${images},${image}" + else + images="${image}" + fi + fi + done + echo "images=${images}" | tee -a "${GITHUB_OUTPUT}" + + if [ -z "${images}" ]; then + echo "::warning::No registries have been selected, no images will be pushed" + echo "images=dry-run-image" | tee -a "${GITHUB_OUTPUT}" + fi + + if [ "${PUSH}" != "true" ]; then + echo "::warning::push=${PUSH}, no images will be pushed" + fi + + - name: Login to GAR + if: ${{ inputs.push == 'true' && steps.registries.outputs.include-gar == 'true' }} + uses: ./_shared-workflows-docker-build-push-image/actions/login-to-gar + + - name: Login to DockerHub + if: ${{ inputs.push == 'true' && steps.registries.outputs.include-dockerhub == 'true' }} + uses: ./_shared-workflows-docker-build-push-image/actions/dockerhub-login + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 + with: + images: ${{ steps.setup-vars.outputs.images }} + tags: ${{ inputs.tags }} + + - name: Setup buildkitd-config + id: buildkitd-config + shell: bash + env: + BUILDKITD_CONFIG_INLINE: ${{ inputs.buildkitd-config-inline }} + BUILDKITD_CONFIG: ${{ inputs.buildkitd-config }} + DEFAULT_BUILDKITD_CONFIG: /etc/buildkitd.toml + RUNNER_ENVIRONMENT: ${{ runner.environment }} + run: | + buildkitd_config="" + buildkitd_config_inline="" + + if [ -n "${BUILDKITD_CONFIG_INLINE}" ]; then + echo "Inline config was provided" + buildkitd_config_inline="${BUILDKITD_CONFIG_INLINE}" + elif [ -n "${BUILDKITD_CONFIG}" ]; then + echo "Config file was provided" + buildkitd_config="${BUILDKITD_CONFIG}" + else + echo "No configs were provided, building default config setting" + if [ "${RUNNER_ENVIRONMENT}" != "self-hosted" ]; then + echo "Not on self hosted runner, no config will be applied" + else + if [ -f "${DEFAULT_BUILDKITD_CONFIG}" ]; then + echo "${DEFAULT_BUILDKITD_CONFIG} exists, using that as default config." + buildkitd_config="${DEFAULT_BUILDKITD_CONFIG}" + else + echo "::warning::${DEFAULT_BUILDKITD_CONFIG} does not exist, no config will be applied" + fi + fi + fi + + echo "buildkitd-config-inline=${buildkitd_config_inline}" | tee -a "${GITHUB_OUTPUT}" + echo "buildkitd-config=${buildkitd_config}" | tee -a "${GITHUB_OUTPUT}" + + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + with: + driver: ${{ inputs.docker-buildx-driver }} + version: latest # see https://github.com/docker/build-push-action/issues/1345#issuecomment-2770572479 + buildkitd-config: ${{ steps.buildkitd-config.outputs.buildkitd-config }} + buildkitd-config-inline: ${{ steps.buildkitd-config.outputs.buildkitd-config-inline }} + + # The `context` input is flagged by Zizmor as a [sink]. This means that with + # the upstream action the user's input to the input ends up in an output, + # and so if it's not handled properly, it could lead to a template injection + # attack. In this action, we do pass this back out via our `metadata` + # output. However, we consider ourselves a proxy, so in that case our job is + # to warn users but not to take any action. + # + # [sink]: https://github.blog/security/application-security/how-to-secure-your-github-actions-workflows-with-codeql/#models + - name: Build the container # zizmor: ignore[template-injection] + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + id: build + with: + build-args: ${{ inputs.build-args }} + build-contexts: ${{ inputs.build-contexts }} + cache-from: ${{ inputs.cache-from }} + cache-to: ${{ inputs.cache-to }} + context: ${{ inputs.context }} + file: ${{ inputs.file }} + labels: ${{ inputs.labels }} + load: ${{ inputs.load == 'true' }} + platforms: ${{ inputs.platforms }} + outputs: ${{ inputs.outputs }} + push: ${{ inputs.push == 'true' }} + secrets: ${{ inputs.secrets }} + ssh: ${{ inputs.ssh }} + tags: ${{ inputs.include-tags-in-push == 'true' && steps.meta.outputs.tags || steps.setup-vars.outputs.images }} + target: ${{ inputs.target }} + + - name: Cleanup checkout directory + if: ${{ !cancelled() }} + shell: bash + run: | + # Check that the directory looks OK before removing it + if ! [ -d "_shared-workflows-docker-build-push-image/.git" ]; then + echo "::warning Not removing shared workflows directory: doesn't look like a git repository" + exit 0 + fi + + rm -rf _shared-workflows-docker-build-push-image + + - name: Delete Google Application Credentials file + if: ${{ always() && inputs.gar-delete-credentials-file == 'true' && env.GOOGLE_APPLICATION_CREDENTIALS != '' }} + shell: sh + run: | + if [ -f "${GOOGLE_APPLICATION_CREDENTIALS}" ]; then + rm -f "${GOOGLE_APPLICATION_CREDENTIALS}" + echo "::notice::Successfully deleted credentials file" + else + echo "::warning::Credentials file not found at ${GOOGLE_APPLICATION_CREDENTIALS}" + fi + env: + GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }} diff --git a/actions/docker-build-push-image/dockerhub-setup-project-vars.sh b/actions/docker-build-push-image/dockerhub-setup-project-vars.sh new file mode 100644 index 000000000..4ae48d842 --- /dev/null +++ b/actions/docker-build-push-image/dockerhub-setup-project-vars.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Required env vars (provided in workflow yaml) +: "${DOCKERHUB_IMAGE:?DOCKERHUB_IMAGE env var is required}" + +echo "image=${DOCKERHUB_REGISTRY}/${DOCKERHUB_IMAGE}" | tee -a "${GITHUB_OUTPUT}" diff --git a/actions/docker-build-push-image/gar-setup-project-vars.sh b/actions/docker-build-push-image/gar-setup-project-vars.sh new file mode 100644 index 000000000..970bb11b6 --- /dev/null +++ b/actions/docker-build-push-image/gar-setup-project-vars.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Optional env vars +: "${GAR_REPO:=}" # default, empty +: "${GAR_IMAGE:=}" # default, empty + +# Required env vars (provided in workflow yaml) +: "${ENVIRONMENT:?ENVIRONMENT env var is required}" +: "${REGISTRY:?REGISTRY env var is required}" +: "${GH_REPO:?GH_REPO env var is required}" + +gh_repo_name="$(echo "${GH_REPO}" | awk -F'/' '{print $2}')" # ex: grafana/repo -> repo + + +######################################## +# Resolve name of Google Artifact Repository +######################################## +gar_repo_name="${GAR_REPO}" +if [ -z "${gar_repo_name}" ]; then + gar_repo_name="docker-${gh_repo_name//_/-}-${ENVIRONMENT}" +fi +echo "gar_repo_name=${gar_repo_name}" + +######################################## +# Resolve Image Name +######################################## +gar_image="${GAR_IMAGE}" +if [ -z "${gar_image}" ]; then + gar_image="${gh_repo_name}" +fi +echo "gar_image=${gar_image}" + +######################################## +# Resolve project +######################################## +case "${ENVIRONMENT}" in + dev) + gar_project="grafanalabs-dev" + ;; + prod) + gar_project="grafanalabs-global" + ;; + *) + echo "❌ Invalid ENVIRONMENT: $ENVIRONMENT (must be 'dev' or 'prod')" >&2 + exit 1 + ;; +esac +echo "gar_project=${gar_project}" + +######################################## +# Build image path +######################################## +gar_image="${REGISTRY}/${gar_project}/${gar_repo_name}/${gar_image}" + +echo "image=${gar_image}" | tee -a "${GITHUB_OUTPUT}" diff --git a/actions/docker-export-digest/README.md b/actions/docker-export-digest/README.md new file mode 100644 index 000000000..c49910873 --- /dev/null +++ b/actions/docker-export-digest/README.md @@ -0,0 +1,43 @@ +# docker-export-digest + +This is a composite GitHub Action used to export a docker digest as a workflow artifact. + +This can be used in conjunction with [docker-build-push-image] and [docker-import-digests-push-manifest] to build +native multi-arch Docker images. + +[docker/build-push-action]: https://github.com/docker/build-push-action +[docker-build-push-image]: ../docker-build-push-image/README.md +[docker-export-digest]: ../docker-export-digest/README.md +[docker-import-digests-push-manifest]: ../docker-import-digests-push-manifest/README.md + + + +```yaml +name: Build a Docker Image + +on: + push: + branches: + - main + +jobs: + upload-digest-as-artifact: + permissions: + contents: read + id-token: write + steps: + - name: Export and upload digest + uses: grafana/shared-workflows/actions/docker-export-digest@docker-export-digest/v0.0.0 + with: + digest: ${{ steps.docker-build-push-image.outputs.digest }} + platform: linux/arm64 +``` + + + +## Inputs + +| Name | Type | Description | +| ---------- | ------ | ---------------------------------------------------------------------------------------------------------- | +| `digest` | String | Docker digest. This is included as an output for `docker-build-push-image` and `docker/build-push-action`. | +| `platform` | String | Docker platform, ex: linux/arm64. | diff --git a/actions/docker-export-digest/action.yaml b/actions/docker-export-digest/action.yaml new file mode 100644 index 000000000..e994ffb84 --- /dev/null +++ b/actions/docker-export-digest/action.yaml @@ -0,0 +1,43 @@ +name: Export and Upload Docker Manifest +description: Composite action to export and upload a docker manifest + +inputs: + digest: + description: | + Docker digest. + This is included as an output for `docker-build-push-image` and `docker/build-push-action`. + required: true + platform: + description: | + Docker platform, ex: linux/arm64. + required: true + +runs: + using: composite + steps: + - name: Prepare + id: prepare + shell: bash + env: + PLATFORM: ${{ inputs.platform }} + run: | + echo "platform-pair=${PLATFORM//\//-}" | tee -a "${GITHUB_OUTPUT}" + + - name: Export digest + shell: sh + run: | + mkdir -p ${{ runner.temp }}/digests + digest="${STEPS_BUILD_OUTPUTS_DIGEST}" + touch "${{ runner.temp }}/digests/${digest#sha256:}" + + ls -lah ${{ runner.temp }}/digests + env: + STEPS_BUILD_OUTPUTS_DIGEST: ${{ inputs.digest }} + + - name: Upload digest + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: digests-${{ steps.prepare.outputs.platform-pair }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + retention-days: 1 diff --git a/actions/docker-import-digests-push-manifest/README.md b/actions/docker-import-digests-push-manifest/README.md new file mode 100644 index 000000000..196e6c5ff --- /dev/null +++ b/actions/docker-import-digests-push-manifest/README.md @@ -0,0 +1,49 @@ +# docker-import-digest-push-manifest + +This is a composite GitHub Action used to import Docker digests from a shared workflow artifact and merge them into a +tagged manifest. + +This can be used in conjunction with [docker-build-push-image] and [docker-export-digest] to build +native multi-arch Docker images. + +[docker/build-push-action]: https://github.com/docker/build-push-action +[docker-build-push-image]: ../docker-build-push-image/README.md +[docker-export-digest]: ../docker-export-digest/README.md +[docker-import-digests-push-manifest]: ../docker-import-digests-push-manifest/README.md + + + +```yaml +name: Build a Docker Image + +on: + push: + branches: + - main + +jobs: + import-and-merge-digest: + permissions: + contents: read + id-token: write + steps: + - name: Download Multi-Arch Digests, Construct and Upload Manifest + uses: grafana/shared-workflows/actions/docker-import-digests-push-manifest@docker-import-digests-push-manifest/v0.0.0 + with: + docker-metadata-json: ${{ needs.docker-build-push-image.outputs.metadatajson }} + gar-environment: "dev" + images: ${{ needs.docker-build-push-image.outputs.images }} + push: true +``` + + + +## Inputs + +| Name | Type | Description | +| ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `docker-metadata-json` | String | Docker metadata JSON, from `docker-build-push-image` or `docker/build-push-action`. | +| `gar-environment` | String | Environment for pushing artifacts (can be either dev or prod). This sets the GAR Project to either `grafanalabs-dev` or `grafanalabs-global`. | +| `images` | String | CSV of Docker images to push. These images should not include tags. Ex: us-docker.pkg.dev/grafanalabs-dev/gar-registry/image-name,docker.io/grafana/dockerhub-image | +| `push` | Boolean | Whether to push the manifest to the configured registries. | +| `tags` | String | List of Docker tags to be pushed. | diff --git a/actions/docker-import-digests-push-manifest/action.yaml b/actions/docker-import-digests-push-manifest/action.yaml new file mode 100644 index 000000000..b441ca52b --- /dev/null +++ b/actions/docker-import-digests-push-manifest/action.yaml @@ -0,0 +1,160 @@ +name: Download and Merge Docker Digests into Manifest +description: Composite action to export and upload a docker manifest + +inputs: + docker-metadata-json: + description: | + Docker metadata JSON, from `docker-build-push-image` or `docker/build-push-action`. + default: "" + gar-environment: + description: | + Environment for pushing artifacts (can be either dev or prod). + This sets the GAR Project to either `grafanalabs-dev` or `grafanalabs-global`. + default: "dev" + images: + description: | + CSV of Docker images to push. These images should not include tags. + Ex: us-docker.pkg.dev/grafanalabs-dev/gar-registry/image-name,docker.io/grafana/dockerhub-image + required: true + push: + description: | + Whether to push the manifest to the configured registries. + default: "false" + tags: + description: | + List of Docker tags to be pushed. + required: true + +runs: + using: composite + steps: + - name: Checkout shared-workflows + env: + action_repo: ${{ github.action_repository }} + action_ref: ${{ github.action_ref }} + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: ${{ env.action_repo }} + ref: ${{ env.action_ref }} + path: _shared-workflows-docker-import-digests-push-manifest + persist-credentials: false + + - name: Download digests + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + path: ${{ runner.temp }}/digests + pattern: digests-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + with: + driver: docker-container + version: latest # see https://github.com/docker/build-push-action/issues/1345#issuecomment-2770572479 + + - name: Prepare vars + id: prepare-vars + if: ${{ inputs.push == 'true' }} + env: + IMAGES: ${{ inputs.images }} + shell: bash + run: | + ################################################################## + # This step parses the input image list to determine if + # docker images or gar images have been passed in... so we + # can determine which systems to login to. + ################################################################## + + set -euo pipefail + + DOCKERHUB_IMAGE=false + GAR_IMAGE=false + + IFS=',' read -ra IMAGE_LIST <<< "${IMAGES}" + + for image in "${IMAGE_LIST[@]}"; do + image="$(echo "$image" | xargs)" # trim spaces + registry="${image%%/*}" # everything before first slash + echo "Verifying image: $image" + echo "Verifying registry: $registry" + + # Default if there's no dot or colon (Docker Hub shorthand) + if [[ "$registry" != *.* && "$registry" != *:* ]]; then + DOCKERHUB_IMAGE=true + fi + + if [[ "$registry" == *".pkg.dev" ]] || [[ "$registry" == *"gcr.io" ]]; then + echo "$image → Google Artifact Registry" + GAR_IMAGE=true + elif [[ "$registry" == "docker.io" ]] || [[ "$registry" == "index.docker.io" ]]; then + echo "$image → DockerHub" + DOCKERHUB_IMAGE=true + else + echo "$image → Other registry ($registry)" + fi + done + + if [[ "$DOCKERHUB_IMAGE" == "true" ]]; then + echo "include-dockerhub=true" | tee -a "${GITHUB_OUTPUT}" + fi + if [[ "$GAR_IMAGE" == "true" ]]; then + echo "include-gar=true" | tee -a "${GITHUB_OUTPUT}" + fi + + - name: Login to GAR + if: ${{ steps.prepare-vars.outputs.include-gar == 'true' }} + uses: ./_shared-workflows-docker-import-digests-push-manifest/actions/login-to-gar + + - name: Login to DockerHub + if: ${{ steps.prepare-vars.outputs.include-dockerhub == 'true' }} + uses: ./_shared-workflows-docker-import-digests-push-manifest/actions/dockerhub-login + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0 + with: + images: ${{ inputs.images }} + tags: ${{ inputs.tags }} + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/digests + shell: bash + env: + IMAGES: ${{ inputs.images }} + PUSH: ${{ inputs.push }} + run: | + set -euo pipefail + echo "ls -lah ${{ runner.temp }}/digests" + ls -lah ${{ runner.temp }}/digests + + if [ "${PUSH}" != "true" ]; then + echo "::warning::push=${PUSH}, no images will be pushed" + fi + + if [ -n "${IMAGES}" ]; then + for image in ${IMAGES//,/ }; do + echo docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf "${image}@sha256:%s " *) + + if [ "${PUSH}" == "true" ]; then + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf "${image}@sha256:%s " *) + else + echo Skipping command since push is set to false. + fi + done + else + echo "::warning::No images to push, skipping imagetools creation" + fi + + - name: Inspect image + shell: bash + if: ${{ inputs.push == 'true' }} + run: | + for tag in $(jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON"); do + echo "" + echo "Inspecting $tag" + docker buildx imagetools inspect "$tag" + done