diff --git a/.github/workflows/build_all_images.yml b/.github/workflows/build_all_images.yml index 8887abdf..98484a0c 100644 --- a/.github/workflows/build_all_images.yml +++ b/.github/workflows/build_all_images.yml @@ -11,14 +11,10 @@ name: build_all_images NO_CACHE: required: true type: boolean -permissions: - attestations: write - contents: read - packages: write - id-token: write +permissions: {} jobs: discover_folders: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 outputs: base_node_folders: ${{ steps.find-folders.outputs.base_node }} node_24_language_folders: ${{ steps.find-folders.outputs.node_24_languages }} @@ -39,8 +35,67 @@ jobs: echo "projects=$project_folders" } >> "$GITHUB_OUTPUT" + build_tool_images: + # build common tool images with a lower scoped github token + # as it uses a 3rd party docker image with github cli installed to verify attestation of tflint binary + # and we dont want to make a high scoped token available to that image + # token needs attestation read so it can verify attestation of tflint binary + name: Build tool images for on ${{ matrix.arch }} + runs-on: '${{ matrix.runner }}' + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-22.04 + - arch: arm64 + runner: ubuntu-22.04-arm + permissions: + contents: read + attestations: read + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + fetch-depth: 0 + persist-credentials: false + - name: build_grype + run: | + make build-grype + docker save "local_grype:latest" -o grype_image.tar + - name: build_syft + run: | + make build-syft + docker save "local_syft:latest" -o syft_image.tar + - name: build_grant + run: | + make build-grant + docker save "local_grant:latest" -o grant_image.tar + + - name: build_tflint + run: | + make build-tflint + docker save "local_tflint:latest" -o tflint_image.tar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + name: Upload docker images + with: + name: docker_artifact_${{ matrix.arch }} + path: | + grype_image.tar + syft_image.tar + grant_image.tar + tflint_image.tar package_base_docker_image: uses: ./.github/workflows/build_multi_arch_image.yml + permissions: + attestations: write + contents: read + packages: write + id-token: write + needs: [build_tool_images] with: tag_latest: ${{ inputs.tag_latest }} docker_tag: ${{ inputs.docker_tag }} @@ -51,6 +106,11 @@ jobs: needs: - package_base_docker_image - discover_folders + permissions: + attestations: write + contents: read + packages: write + id-token: write strategy: fail-fast: false matrix: @@ -68,6 +128,11 @@ jobs: - package_base_docker_image - package_base_node_images - discover_folders + permissions: + attestations: write + contents: read + packages: write + id-token: write strategy: fail-fast: false matrix: @@ -84,6 +149,11 @@ jobs: needs: - package_node_24_language_docker_images - discover_folders + permissions: + attestations: write + contents: read + packages: write + id-token: write strategy: fail-fast: false matrix: diff --git a/.github/workflows/build_multi_arch_image.yml b/.github/workflows/build_multi_arch_image.yml index eaaac733..498fc4f1 100644 --- a/.github/workflows/build_multi_arch_image.yml +++ b/.github/workflows/build_multi_arch_image.yml @@ -41,19 +41,6 @@ jobs: - arch: arm64 runner: ubuntu-22.04-arm steps: - - name: Free Disk Space for Docker - uses: endersonmenezes/free-disk-space@7901478139cff6e9d44df5972fd8ab8fcade4db1 - with: - remove_android: true - remove_dotnet: true - remove_haskell: true - remove_tool_cache: true - rm_cmd: rm - remove_packages: >- - azure-cli google-cloud-cli microsoft-edge-stable - google-chrome-stable firefox postgresql* temurin-* *llvm* mysql* - dotnet-sdk-* - remove_packages_one_command: true - name: Login to github container registry uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 with: @@ -69,11 +56,30 @@ jobs: uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f with: node-version: '24.14.0' + - name: docker_artifact download + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: docker_artifact_${{ matrix.arch }} + path: images/ + - name: extract docker images + run: | + for image in images/*.tar; do + docker load -i "$image" + done + rm -rf images - name: setup syft and grype run: | mkdir -p "$RUNNER_TEMP/bin" - docker build --output="$RUNNER_TEMP/bin" -f "src/base/.devcontainer/Dockerfile.syft" src/base/.devcontainer/ - docker build --output="$RUNNER_TEMP/bin" -f "src/base/.devcontainer/Dockerfile.grype" src/base/.devcontainer/ + id=$(docker create local_grype:latest) + docker cp "$id":/grype - | tar -xOf - grype > "$RUNNER_TEMP/bin/grype" + chmod +x "$RUNNER_TEMP/bin/grype" + docker rm -v "$id" + + mkdir -p "$RUNNER_TEMP/bin" + id=$(docker create local_syft:latest) + docker cp "$id":/syft - | tar -xOf - syft > "$RUNNER_TEMP/bin/syft" + chmod +x "$RUNNER_TEMP/bin/syft" + docker rm -v "$id" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" - name: make install run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff2f1124..aebd81ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,13 @@ jobs: contents: read packages: read quality_checks: - uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@f2d4d6942115472d3f08316cd25f400b02a9dc69 + uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@e798d5aee897de6f7dc387dd5623fcd9ba4c8929 needs: - get_config_values + permissions: + contents: read + packages: read + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} secrets: @@ -35,6 +39,7 @@ jobs: build_all_images: needs: - tag_release + - get_config_values uses: ./.github/workflows/build_all_images.yml permissions: attestations: write diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 96a92566..9ce6472e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -23,11 +23,15 @@ jobs: contents: read packages: read quality_checks: - uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@f2d4d6942115472d3f08316cd25f400b02a9dc69 + uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@e798d5aee897de6f7dc387dd5623fcd9ba4c8929 needs: - get_config_values with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + permissions: + contents: read + packages: read + id-token: write secrets: SONAR_TOKEN: '${{ secrets.SONAR_TOKEN }}' pr_title_format_check: @@ -80,6 +84,7 @@ jobs: needs: - get_issue_number - get_commit_id + - get_config_values uses: ./.github/workflows/build_all_images.yml permissions: attestations: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61d80d0f..8af6a0e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,9 +15,13 @@ jobs: contents: read packages: read quality_checks: - uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@f2d4d6942115472d3f08316cd25f400b02a9dc69 + uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@e798d5aee897de6f7dc387dd5623fcd9ba4c8929 needs: - get_config_values + permissions: + contents: read + packages: read + id-token: write with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} secrets: @@ -38,6 +42,7 @@ jobs: build_all_images: needs: - tag_release + - get_config_values uses: ./.github/workflows/build_all_images.yml permissions: attestations: write diff --git a/Makefile b/Makefile index e2aed6a9..140fdba4 100644 --- a/Makefile +++ b/Makefile @@ -12,14 +12,15 @@ guard-%: .PHONY: install install-python install-node install-hooks build-base-image build-node-24-image build-node-24-python-3-10-image build-node-24-python-3-12-image build-node-24-python-3-13-image build-node-24-python-3-14-image \ build-eps-storage-terraform-image build-eps-data-extract-image build-fhir-facade-image build-node-24-python-3-14-golang-1-24-image build-node-24-python-3-14-java-24-image \ - build-regression-tests-image build-all build-image build-githubactions-image scan-image scan-image-json shell-image lint test lint-githubactions lint-githubaction-scripts clean + build-regression-tests-image build-all build-image build-githubactions-image scan-image scan-image-json shell-image lint test lint-githubactions lint-githubaction-scripts clean \ + build-syft build-grype build-grant build-tflint install: install-python install-node install-hooks install-python: poetry install install-node: - npm install + npm ci --ignore-scripts install-hooks: install-python poetry run pre-commit install --install-hooks --overwrite @@ -43,7 +44,7 @@ build-node-24-python-3-14-image: CONTAINER_NAME=node_24_python_3_14 BASE_VERSION_TAG=local-build BASE_FOLDER=languages IMAGE_TAG=local-build $(MAKE) build-image build-eps-storage-terraform-image: - CONTAINER_NAME=eps_storage_terraform BASE_VERSION_TAG=local-build BASE_FOLDER=projects IMAGE_TAG=local-build $(MAKE) build-image + CONTAINER_NAME=eps-storage-terraform BASE_VERSION_TAG=local-build BASE_FOLDER=projects IMAGE_TAG=local-build $(MAKE) build-image build-eps-data-extract-image: CONTAINER_NAME=eps_data_extract BASE_VERSION_TAG=local-build BASE_FOLDER=projects IMAGE_TAG=local-build $(MAKE) build-image @@ -65,14 +66,37 @@ build-all: build-base-image build-node-24-image build-node-24-python-3-10-image build-regression-tests-image build-syft: - docker build -f src/base/.devcontainer/Dockerfile.syft --tag local_syft:latest src/base/.devcontainer/ + @if docker image inspect local_syft:latest >/dev/null 2>&1; then \ + echo "Image local_syft:latest already exists. Skipping build."; \ + else \ + docker build -f src/base/.devcontainer/Dockerfile.syft --tag local_syft:latest src/base/.devcontainer/; \ + fi build-grype: - docker build -f src/base/.devcontainer/Dockerfile.grype --tag local_grype:latest src/base/.devcontainer/ + @if docker image inspect local_grype:latest >/dev/null 2>&1; then \ + echo "Image local_grype:latest already exists. Skipping build."; \ + else \ + docker build -f src/base/.devcontainer/Dockerfile.grype --tag local_grype:latest src/base/.devcontainer/; \ + fi build-grant: - docker build -f src/base/.devcontainer/Dockerfile.grant --tag local_grant:latest src/base/.devcontainer/ + @if docker image inspect local_grant:latest >/dev/null 2>&1; then \ + echo "Image local_grant:latest already exists. Skipping build."; \ + else \ + docker build -f src/base/.devcontainer/Dockerfile.grant --tag local_grant:latest src/base/.devcontainer/; \ + fi + +build-tflint: + @if docker image inspect local_tflint:latest >/dev/null 2>&1; then \ + echo "Image local_tflint:latest already exists. Skipping build."; \ + else \ + docker buildx build \ + --secret id=GH_TOKEN,env=GITHUB_TOKEN \ + -f src/projects/eps-storage-terraform/.devcontainer/Dockerfile.tflint \ + --tag local_tflint:latest \ + src/projects/eps-storage-terraform/.devcontainer/; \ + fi -build-image: build-syft build-grype build-grant guard-CONTAINER_NAME guard-BASE_VERSION_TAG guard-BASE_FOLDER guard-IMAGE_TAG +build-image: build-syft build-grype build-grant build-tflint guard-CONTAINER_NAME guard-BASE_VERSION_TAG guard-BASE_FOLDER guard-IMAGE_TAG workspace_folder="$${CONTAINER_NAME}"; \ case "$${CONTAINER_NAME}" in \ eps_*) workspace_folder="$$(printf '%s' "$${CONTAINER_NAME}" | tr '_' '-')" ;; \ diff --git a/src/base/.devcontainer/.tool-versions b/src/base/.devcontainer/.tool-versions index 7aaf5f8b..20eb3b3d 100644 --- a/src/base/.devcontainer/.tool-versions +++ b/src/base/.devcontainer/.tool-versions @@ -1,5 +1,5 @@ shellcheck 0.11.0 direnv 2.37.1 -actionlint 1.7.11 +actionlint 1.7.12 ruby 3.3.0 -yq 4.52.4 +yq 4.52.5 diff --git a/src/projects/eps-storage-terraform/.devcontainer/Dockerfile b/src/projects/eps-storage-terraform/.devcontainer/Dockerfile index 8226af24..c64245dd 100644 --- a/src/projects/eps-storage-terraform/.devcontainer/Dockerfile +++ b/src/projects/eps-storage-terraform/.devcontainer/Dockerfile @@ -1,6 +1,7 @@ ARG BASE_VERSION_TAG=latest ARG BASE_IMAGE=ghcr.io/nhsdigital/eps-devcontainers/node_24_python_3_13:${BASE_VERSION_TAG} +FROM local_tflint:latest AS tflint-build FROM ${BASE_IMAGE} ARG SCRIPTS_DIR=/usr/local/share/eps @@ -26,6 +27,7 @@ USER root COPY --chmod=755 scripts ${SCRIPTS_DIR}/${CONTAINER_NAME} WORKDIR ${SCRIPTS_DIR}/${CONTAINER_NAME} RUN ./root_install.sh +COPY --from=tflint-build /tflint /usr/local/bin/tflint USER vscode diff --git a/src/projects/eps-storage-terraform/.devcontainer/Dockerfile.tflint b/src/projects/eps-storage-terraform/.devcontainer/Dockerfile.tflint new file mode 100644 index 00000000..d6a1e223 --- /dev/null +++ b/src/projects/eps-storage-terraform/.devcontainer/Dockerfile.tflint @@ -0,0 +1,13 @@ +FROM serversideup/github-cli:2.89.0 AS build +ARG TARGETARCH +ARG TFLINT_VERSION="v0.61.0" +COPY --chmod=755 scripts/install_tflint.sh /tmp/install_tflint.sh +RUN --mount=type=secret,id=GH_TOKEN,env=GH_TOKEN \ + INSTALL_DIR=/tmp/tflint/ \ + ARCH="${TARGETARCH}" \ + VERSION="${TFLINT_VERSION}" \ + /tmp/install_tflint.sh + +FROM scratch +COPY --from=build /tmp/tflint/tflint /tflint +ENTRYPOINT ["/tflint"] diff --git a/src/projects/eps-storage-terraform/.devcontainer/scripts/install_tflint.sh b/src/projects/eps-storage-terraform/.devcontainer/scripts/install_tflint.sh new file mode 100755 index 00000000..1f62116e --- /dev/null +++ b/src/projects/eps-storage-terraform/.devcontainer/scripts/install_tflint.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -euo pipefail +export DEBIAN_FRONTEND=noninteractive + +DEFAULT_INSTALL_DIR="/usr/local/bin" +INSTALL_DIR="${INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" + +case "${TARGETARCH:-}" in + amd64|arm64) + TFLINT_ARCH="${TARGETARCH}" + ;; + *) + echo "Unsupported or missing TARGETARCH: '${TARGETARCH:-}'" + echo "Expected one of: amd64, arm64" + exit 1 + ;; +esac + +if ! command -v curl >/dev/null 2>&1 || ! command -v unzip >/dev/null 2>&1; then + apt-get update + apt-get install -y --no-install-recommends curl unzip ca-certificates +fi + +if ! command -v gh >/dev/null 2>&1; then + echo "GitHub CLI (gh) is required for attestation verification but was not found" + exit 1 +fi + +TFLINT_URL="https://github.com/terraform-linters/tflint/releases/download/${TFLINT_VERSION}/tflint_linux_${TFLINT_ARCH}.zip" +TFLINT_ASSET_NAME="tflint_linux_${TFLINT_ARCH}.zip" +CHECKSUMS_URL="https://github.com/terraform-linters/tflint/releases/download/${TFLINT_VERSION}/checksums.txt" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "${tmp_dir}"' EXIT + +curl -fsSL "${CHECKSUMS_URL}" -o "${tmp_dir}/checksums.txt" +gh attestation verify "${tmp_dir}/checksums.txt" -R terraform-linters/tflint + +curl -fsSL "${TFLINT_URL}" -o "${tmp_dir}/${TFLINT_ASSET_NAME}" +( + cd "${tmp_dir}" + sha256sum --ignore-missing -c checksums.txt +) + +unzip -q "${tmp_dir}/${TFLINT_ASSET_NAME}" -d "${tmp_dir}" + +mkdir -p "$INSTALL_DIR" +install -m 0755 "$tmp_dir/tflint" "${INSTALL_DIR}/tflint" diff --git a/src/projects/eps-storage-terraform/.devcontainer/scripts/root_install.sh b/src/projects/eps-storage-terraform/.devcontainer/scripts/root_install.sh index 474c45b0..0201843b 100755 --- a/src/projects/eps-storage-terraform/.devcontainer/scripts/root_install.sh +++ b/src/projects/eps-storage-terraform/.devcontainer/scripts/root_install.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -e +set -euo pipefail # clean up apt-get clean