diff --git a/dist/tools/buildsystem_sanity_check/check.sh b/dist/tools/buildsystem_sanity_check/check.sh index 124b84d0b2..5a74658f1f 100755 --- a/dist/tools/buildsystem_sanity_check/check.sh +++ b/dist/tools/buildsystem_sanity_check/check.sh @@ -16,6 +16,7 @@ : "${RIOTBASE:="$(cd "$(dirname "$0")/../../../" || exit; pwd)"}" : "${RIOTTOOLS:=${RIOTBASE}/dist/tools}" +: "${RIOTMAKE:=${RIOTBASE}/makefiles}" # not running shellcheck with -x in the CI --> disable SC1091 # shellcheck disable=SC1091 . "${RIOTTOOLS}"/ci/github_annotate.sh @@ -380,6 +381,28 @@ check_tests_application_path() { find tests/ -type f "${patterns[@]}" | error_with_message "Invalid application path in tests/" } +check_pinned_docker_version_is_up_to_date() { + local pinned_digest + local pinned_repo_digest + local upstream_digest + local upstream_repo_digest + pinned_digest="$(awk '/^DOCKER_TESTED_IMAGE_ID := (.*)$/ { print substr($0, index($0, $3)); exit }' "$RIOTMAKE/docker.inc.mk")" + pinned_repo_digest="$(awk '/^DOCKER_TESTED_IMAGE_REPO_DIGEST := (.*)$/ { print substr($0, index($0, $3)); exit }' "$RIOTMAKE/docker.inc.mk")" + # not using docker and jq here but a python script to not have to install + # more stuff for the static test docker image + IFS=' ' read -r upstream_digest upstream_repo_digest <<< "$("$RIOTTOOLS/buildsystem_sanity_check/get_dockerhub_digests.py" "riot/riotbuild")" + + if [ "$pinned_digest" != "$upstream_digest" ]; then + git -C "${RIOTBASE}" grep -n '^DOCKER_TESTED_IMAGE_ID :=' "$RIOTMAKE/docker.inc.mk" \ + | error_with_message "Update docker image SHA256 to ${upstream_digest}" + fi + + if [ "$pinned_repo_digest" != "$upstream_repo_digest" ]; then + git -C "${RIOTBASE}" grep -n '^DOCKER_TESTED_IMAGE_REPO_DIGEST :=' "$RIOTMAKE/docker.inc.mk" \ + | error_with_message "Update manifest digest to ${upstream_repo_digest}" + fi +} + error_on_input() { ! grep '' } @@ -402,6 +425,7 @@ all_checks() { check_no_riot_config check_stderr_null check_tests_application_path + check_pinned_docker_version_is_up_to_date } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then diff --git a/dist/tools/buildsystem_sanity_check/get_dockerhub_digests.py b/dist/tools/buildsystem_sanity_check/get_dockerhub_digests.py new file mode 100755 index 0000000000..4ac75a1910 --- /dev/null +++ b/dist/tools/buildsystem_sanity_check/get_dockerhub_digests.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Command line utility to get the image sha256 sum and the manifest sha256 sum +of the latest image at dockerhub. +""" +import http.client +import json +import sys + + +def get_docker_token(repo): + """ + Get an API access token for the docker registry + + :param repo: the repository the API token should be valid for + :type repo: str + :return: the access token to use + :rtype: str + """ + conn = http.client.HTTPSConnection("auth.docker.io") + conn.request("GET", + f"/token?service=registry.docker.io&scope=repository:{repo}:pull", + headers={"Accept": "*/*"}) + resp = conn.getresponse() + if resp.status != 200: + raise Exception(f"Tried to get a docker token, but auth.docker.io " + f"replied with {resp.status} {resp.reason}") + decoded = json.loads(resp.read()) + conn.close() + return decoded["token"] + + +def get_manifest(repo, tag="latest", token=None): + """ + Get the manifest of the given repo + + :param repo: The repository to get the latest manifest of + :type repo: str + :param tag: The tag to get the manifest of. (Default: "latest") + :type tag: str + :param token: The authorization token to use for the given repo. + (Default: get a fresh one.) + :type token: str + :return: the parsed manifast + :rtype: dict + """ + token = get_docker_token(repo) if token is None else token + conn = http.client.HTTPSConnection("index.docker.io") + hdrs = { + "Accept": "application/vnd.docker.distribution.manifest.v2+json", + "Authorization": f"Bearer {token}" + } + conn.request("GET", f"/v2/{repo}/manifests/{tag}", headers=hdrs) + resp = conn.getresponse() + if resp.status != 200: + raise Exception(f"Tried to get a docker manifest, but " + f"index.docker.io replied with {resp.status} " + f"{resp.reason}") + repo_digest = resp.getheader("ETag")[len("\"sha256:"):-1] + decoded = json.loads(resp.read()) + conn.close() + return (decoded, repo_digest) + + +def get_upstream_digests(repo, tag="latest", token=None): + """ + Get the SHA256 hash of the latest image of the given repo at dockerhub + as string of hex digests + + :param repo: The repository to get the hash from + :type repo: str + :param tag: The tag to get the manifest of. (Default: "latest") + :type tag: str + :param token: The authorization token to use for the given repo. + (Default: get a fresh one.) + :type token: str + :return: A 2-tuple of the image digest and the repo digest + :rtype: (str, str) + """ + token = get_docker_token(repo) if token is None else token + manifest, repo_digest = get_manifest(repo, tag=tag, token=token) + digest = manifest["config"]["digest"] + return (digest[len("sha256:"):], repo_digest) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + sys.exit(f"Usage {sys.argv[0]} ") + + digest, repo_digest = get_upstream_digests(sys.argv[1]) + print(f"{digest} {repo_digest}")