diff --git a/.github/actions/lint-terraform/action.yaml b/.github/actions/lint-terraform/action.yaml index 71c61134..17cf2b8c 100644 --- a/.github/actions/lint-terraform/action.yaml +++ b/.github/actions/lint-terraform/action.yaml @@ -10,8 +10,22 @@ runs: - name: "Install Terraform binary" shell: bash run: | - asdf plugin add terraform || true - asdf install terraform || true + if command -v mise >/dev/null 2>&1 && [[ -f mise.toml ]]; then + MISE_LOCKED=1 mise install terraform + # TODO: CCM-25293 Remove asdf compatibility. + elif command -v asdf >/dev/null 2>&1; then + if ! asdf plugin list | grep -qx terraform; then + asdf plugin add terraform + fi + asdf install terraform + elif ! command -v terraform >/dev/null 2>&1; then + echo "Terraform is unavailable: configure mise or asdf, or provide terraform on PATH." >&2 + exit 1 + fi + command -v terraform >/dev/null 2>&1 || { + echo "Terraform installation failed; terraform is not on PATH." >&2 + exit 1 + } - name: "Check Terraform format" shell: bash run: | diff --git a/docs/github-actions/lint-terraform.md b/docs/github-actions/lint-terraform.md index bb98340d..99715902 100644 --- a/docs/github-actions/lint-terraform.md +++ b/docs/github-actions/lint-terraform.md @@ -23,12 +23,17 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup mise + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + - name: Lint Terraform uses: NHSDigital/nhs-notify-shared-modules/.github/actions/lint-terraform@v1.0.0 ``` ### Details +- The caller must set up a version manager before this action. For migrated repositories, set up mise as shown above. Legacy repositories should retain their existing version-manager setup. +- Installs Terraform from `mise.lock` when the checkout has `mise.toml`; otherwise installs it using the legacy version manager and `.tool-versions`. If neither manager is available, Terraform must already be on `PATH`. - Tool: `terraform fmt` - Checks: Terraform code formatting - Validates: HCL syntax and style diff --git a/scripts/docker/docker.lib.sh b/scripts/docker/docker.lib.sh index 18787105..1681fc69 100644 --- a/scripts/docker/docker.lib.sh +++ b/scripts/docker/docker.lib.sh @@ -13,7 +13,7 @@ set -euo pipefail # Arguments (provided as environment variables): # DOCKER_IMAGE=ghcr.io/org/repo # Docker image name # DOCKER_TITLE="My Docker image" # Docker image title -# TOOL_VERSIONS=$project_dir/.tool-versions # Path to the tool versions file +# TOOL_VERSIONS=$project_dir/.docker-versions # Optional override for image versions # ============================================================================== # Functions to be used with custom images. @@ -144,8 +144,8 @@ function version-create-effective-file() { # ============================================================================== # Functions to be used with external images. -# Retrieve the Docker image version from the '.tool-versions' file and pull the -# image if required. This function is to be used in conjunction with the +# Retrieve the Docker image version from '.docker-versions', falling back to +# '.tool-versions' during migration, then pull the image if required. Use with the # external images and it prevents Docker from downloading an image each time it # is used, since the digest is not stored locally for compressed images. To # optimise, the solution is to pull the image using its digest and then tag it, @@ -157,20 +157,34 @@ function version-create-effective-file() { function docker-get-image-version-and-pull() { # E.g. for the given entry "# docker/ghcr.io/org/image 1.2.3@sha256:hash" in - # the '.tool-versions' file, the following variables will be set to: + # the '.docker-versions' file, the following variables will be set to: # name="ghcr.io/org/image" # version="1.2.3@sha256:hash" # tag="1.2.3" # digest="sha256:hash" - # Get the image full version from the '.tool-versions' file, - # match it by name and version regex, if given. - local versions_file="${TOOL_VERSIONS:=$(git rev-parse --show-toplevel)/.tool-versions}" local version="latest" - if [ -f "$versions_file" ]; then - line=$(grep "docker/${name} " "$versions_file" | sed "s/^#\s*//; s/\s*#.*$//" | grep "${match_version:-".*"}") - [ -n "$line" ] && version=$(echo "$line" | awk '{print $2}') + local repository_root + local versions_file + local resolved_version + local -a versions_files + repository_root=$(git rev-parse --show-toplevel) + if [ -n "${TOOL_VERSIONS:-}" ]; then + case "$TOOL_VERSIONS" in + *.docker-versions*) versions_files=("$TOOL_VERSIONS" "${TOOL_VERSIONS/.docker-versions/.tool-versions}") ;; + *.tool-versions*) versions_files=("${TOOL_VERSIONS/.tool-versions/.docker-versions}" "$TOOL_VERSIONS") ;; + *) versions_files=("$TOOL_VERSIONS") ;; + esac + else + versions_files=("$repository_root/.docker-versions" "$repository_root/.tool-versions") fi + for versions_file in "${versions_files[@]}"; do + resolved_version=$(_docker-image-version-from-file "$versions_file" "$name" "${match_version:-.*}") || true + if [ -n "$resolved_version" ]; then + version="$resolved_version" + break + fi + done # Split the image version into two, tag name and digest sha256. local tag="$(echo "$version" | sed 's/@.*$//')" @@ -218,13 +232,33 @@ function _create-effective-dockerfile() { function _replace-image-latest-by-specific-version() { local dir=${dir:-$PWD} - local versions_file="${TOOL_VERSIONS:=$(git rev-parse --show-toplevel)/.tool-versions}" + local repository_root + local versions_file + local -a versions_files local dockerfile="${dir}/Dockerfile.effective" local build_datetime=${BUILD_DATETIME:-$(date -u +'%Y-%m-%dT%H:%M:%S%z')} - if [ -f "$versions_file" ]; then + repository_root=$(git rev-parse --show-toplevel) + if [ -n "${TOOL_VERSIONS:-}" ]; then + case "$TOOL_VERSIONS" in + *.docker-versions*) versions_files=("$TOOL_VERSIONS" "${TOOL_VERSIONS/.docker-versions/.tool-versions}") ;; + *.tool-versions*) versions_files=("${TOOL_VERSIONS/.tool-versions/.docker-versions}" "$TOOL_VERSIONS") ;; + *) versions_files=("$TOOL_VERSIONS") ;; + esac + else + versions_files=("$repository_root/.docker-versions" "$repository_root/.tool-versions") + fi + if [ "${#versions_files[@]}" -gt 0 ]; then # First, list the entries specific for Docker to take precedence, then the rest but exclude comments - content=$(grep " docker/" "$versions_file"; grep -v " docker/" "$versions_file" ||: | grep -v "^#") + content="" + for versions_file in "${versions_files[@]}"; do + if [ -f "$versions_file" ]; then + content+=$(grep " docker/" "$versions_file" || true) + content+=$'\n' + content+=$(grep -v " docker/" "$versions_file" | grep -v "^#" || true) + content+=$'\n' + fi + done echo "$content" | while IFS= read -r line; do [ -z "$line" ] && continue line=$(echo "$line" | sed "s/^#\s*//; s/\s*#.*$//" | sed "s;docker/;;") @@ -252,6 +286,19 @@ function _replace-image-latest-by-specific-version() { sed -Ei "/# hadolint ignore=DL3007$/d" "${dir}/Dockerfile.effective" } +function _docker-image-version-from-file() { + + local versions_file="$1" + local image_name="$2" + local match_version="$3" + local line + + [ -f "$versions_file" ] || return 1 + line=$(grep "docker/${image_name} " "$versions_file" | sed "s/^#\\s*//; s/\\s*#.*$//" | grep "$match_version" || true) + [ -n "$line" ] || return 1 + echo "$line" | awk '{print $2}' +} + # Append metadata to the end of Dockerfile. # Arguments (provided as environment variables): # dir=[path to the image directory where the Dockerfile is located, default is '.'] diff --git a/scripts/docker/tests/docker.test.sh b/scripts/docker/tests/docker.test.sh index 8f487b8f..ba73a983 100755 --- a/scripts/docker/tests/docker.test.sh +++ b/scripts/docker/tests/docker.test.sh @@ -1,5 +1,5 @@ #!/bin/bash -# shellcheck disable=SC1091,SC2034,SC2317 +# shellcheck disable=SC1091,SC2034,SC2317,SC2329 # WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead. @@ -28,6 +28,7 @@ function main() { tests=( \ test-docker-build \ test-docker-image-from-signature \ + test-docker-legacy-tool-versions \ test-docker-version-file \ test-docker-test \ test-docker-run \ @@ -74,12 +75,54 @@ function test-docker-build() { function test-docker-image-from-signature() { # Arrange - TOOL_VERSIONS="$(git rev-parse --show-toplevel)/scripts/docker/tests/.tool-versions.test" + local test_root + local result=0 + test_root=$(mktemp -d) + cp .tool-versions.test "$test_root/.docker-versions.test" cp Dockerfile Dockerfile.effective # Act - _replace-image-latest-by-specific-version + TOOL_VERSIONS="$test_root/.docker-versions.test" _replace-image-latest-by-specific-version || result=$? # Assert - grep -q "FROM python:.*-alpine.*@sha256:.*" Dockerfile.effective && return 0 || return 1 + if [ "$result" -eq 0 ]; then + grep -q "FROM python:.*-alpine.*@sha256:.*" Dockerfile.effective || result=$? + fi + rm -rf "$test_root" + return "$result" +} + +function test-docker-legacy-tool-versions() { + + local legacy_root + legacy_root=$(mktemp -d) + git -C "$legacy_root" init -q || return 1 + git -C "$legacy_root" -c user.name=test -c user.email=test@example.com commit --allow-empty -qm test || return 1 + cp Dockerfile "$legacy_root/Dockerfile" + cp .tool-versions.test "$legacy_root/.tool-versions" + cp Dockerfile "$legacy_root/Dockerfile.effective" + + ( + cd "$legacy_root" || exit 1 + unset TOOL_VERSIONS + _replace-image-latest-by-specific-version + grep -q "FROM python:.*-alpine.*@sha256:.*" Dockerfile.effective || exit 1 + cp Dockerfile Dockerfile.effective + TOOL_VERSIONS="$legacy_root/.docker-versions" _replace-image-latest-by-specific-version + grep -q "FROM python:.*-alpine.*@sha256:.*" Dockerfile.effective || exit 1 + printf '%s\n' '# docker/python 9.9-alpine@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > .docker-versions + cp Dockerfile Dockerfile.effective + _replace-image-latest-by-specific-version + grep -q "FROM python:9.9-alpine@sha256:aaaaaaaa" Dockerfile.effective || exit 1 + docker() { + case "$1" in + images) return 0 ;; + pull | tag) return 0 ;; + esac + } + name="python" docker-get-image-version-and-pull | grep -q "python:9\\.9-alpine@sha256:" + ) + local result=$? + rm -rf "$legacy_root" + return "$result" } function test-docker-version-file() {