From 1b681b420c6b0723333a3040182d88e4b5578746 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Mon, 5 Feb 2024 15:52:52 -0700 Subject: [PATCH 1/3] Actually push everywhere, build manifests --- tag-state/calculate-tag-state.py | 180 ++++++++++++++++++++++++++++--- 1 file changed, 167 insertions(+), 13 deletions(-) diff --git a/tag-state/calculate-tag-state.py b/tag-state/calculate-tag-state.py index 32be7e53..00b5c75d 100755 --- a/tag-state/calculate-tag-state.py +++ b/tag-state/calculate-tag-state.py @@ -8,15 +8,23 @@ import re import signal import sys +import subprocess -split_version_re = re.compile("[.+-]") +# requires docker be available on the CLI + +# constants +SPLIT_VERSION_REGEX = re.compile("[.+-]") +NEEDS_MANIFEST = "NEEDS_MANIFEST" +PENDING = "PENDING" +SUCCESS = "SUCCESS" +FAILED = "FAILED" # distutils.version.LooseVersion is deprecated. packaging.version is the recommended replacement but I'd like to # keep this script vanilla. This is an attempt at re-implementation good enough for our purposes. class LooseVersion: def __init__(self, s): - self.vs = split_version_re.split(s) + self.vs = SPLIT_VERSION_REGEX.split(s) self.n = max(len(x) for x in self.vs) def cmp(self, other, op): @@ -81,6 +89,10 @@ def read_buildlog_file(buildlog_file_path: Path) -> dict: print_error(f"Buildlog not found at {parsed_args.buildlog_file_path}") +def semver_sorted_dict(d: dict) -> dict: + return dict(sorted(d.items(), key=lambda i: LooseVersion(i[0]))) + + def build_tag_state(buildlog_data: list) -> dict: tag_state = {} for buildlog_entry in buildlog_data: @@ -90,26 +102,51 @@ def build_tag_state(buildlog_data: list) -> dict: major, minor, patch, arch = pad_none( version.replace(".", "-").split("-"), 4 ) + manifest_semver = f"{major}.{minor}.{patch}" + medium_tag = f"{major}.{minor}" if arch: - tag_state[version] = digest - # TODO: need to create a manifest that gets all the else tags of both digests + tag_state[version] = {"digest": digest, "status": PENDING} + tag_state[major] = {"digest": NEEDS_MANIFEST, "status": PENDING} + tag_state[medium_tag] = { + "digest": NEEDS_MANIFEST, + "status": PENDING, + } + tag_state[manifest_semver] = { + "digest": NEEDS_MANIFEST, + "status": PENDING, + } else: - if f"{major}.{minor}.{patch}" in tag_state: - tag_state[f"{major}.{minor}.{patch}"] = digest + if manifest_semver in tag_state: + # if the current digest for our major or minor tag is the previous version of this container, update it + if ( + tag_state[major]["digest"] + is tag_state[manifest_semver]["digest"] + ): + tag_state[major]["digest"] = digest + if ( + tag_state[medium_tag]["digest"] + is tag_state[manifest_semver]["digest"] + ): + tag_state[medium_tag]["digest"] = digest + # update the full tag to the new version of the container always + tag_state[manifest_semver] = {"digest": digest, "status": PENDING} else: - tag_state[major] = digest - tag_state[f"{major}.{minor}"] = digest - tag_state[f"{major}.{minor}.{patch}"] = digest + tag_state[major] = {"digest": digest, "status": PENDING} + tag_state[f"{major}.{minor}"] = { + "digest": digest, + "status": PENDING, + } + tag_state[manifest_semver] = {"digest": digest, "status": PENDING} else: print_error( f"Buildlog entry found without associated container hash: {buildlog_entry}" ) - return tag_state + return semver_sorted_dict(tag_state) -def semver_sorted_dict(d: dict) -> str: +def pretty_printable_dict(d: dict) -> str: return json.dumps( - dict(sorted(tag_state.items(), key=lambda i: LooseVersion(i[0]))), + d, indent=4, sort_keys=False, ) @@ -121,4 +158,121 @@ def semver_sorted_dict(d: dict) -> str: parsed_args = arg_parser.parse_args(sys.argv[1:]) buildlog_data = read_buildlog_file(parsed_args.buildlog_file_path) tag_state = build_tag_state(buildlog_data) - print(semver_sorted_dict(tag_state)) + print(pretty_printable_dict(tag_state)) + # tag and push everything that doesn't need a manifest + image_name = parsed_args.buildlog_file_path.stem + for key, value in tag_state.items(): + if value["digest"] is not NEEDS_MANIFEST: + # Errors will show up here where containers/versions have been removed either because of our retention + # policy or ones that were yanked. + image_name_by_digest = ( + f"gcr.io/ironcore-images/{image_name}@sha256:{value['digest']}" + ) + pull = subprocess.run(["docker", "image", "pull", image_name_by_digest]) + # tag with the new value + if pull.returncode is 0: + tag = subprocess.run( + [ + "docker", + "image", + "tag", + image_name_by_digest, + f"gcr.io/ironcore-images/{image_name}:{key}", + ] + ) + if tag.returncode is 0: + push = subprocess.run( + ["docker", "push", f"gcr.io/ironcore-images/{image_name}:{key}"] + ) + if push.returncode is 0: + value["status"] = SUCCESS + continue + value["status"] = FAILED + # print what was tagged + successfully_tagged = { + k: v["digest"] for k, v in tag_state.items() if v["status"] is SUCCESS + } + failed_tags = { + k: v["digest"] for k, v in tag_state.items() if v["status"] is FAILED + } + needs_manifest = { + k: v["digest"] for k, v in tag_state.items() if v["digest"] is NEEDS_MANIFEST + } + print(pretty_printable_dict(successfully_tagged)) + print_error(pretty_printable_dict(failed_tags)) + print(pretty_printable_dict(needs_manifest)) + # use the tags just pushed to create manifests + link_manifest = {} + for key in needs_manifest: + # we expect no arch tags at this point + major, minor, patch = pad_none(key.split("."), 3) + # if there's a patch we need to create a manifest for it + if patch: + gcr_image = f"gcr.io/ironcore-images/{image_name}:{key}" + # all our images that need manifests have arm64/amd64 versions + create_manifest = subprocess.run( + [ + "docker", + "manifest", + "create", + gcr_image, + f"{gcr_image}-arm64", + f"{gcr_image}-amd64", + ] + ) + if create_manifest.returncode is 0: + annotate_manifest_arm = subprocess.run( + [ + "docker", + "manifest", + "annotate", + gcr_image, + f"{gcr_image}-arm64", + "--arch", + "arm64", + ] + ) + annotate_manifest_amd = subprocess.run( + [ + "docker", + "manifest", + "annotate", + gcr_image, + f"{gcr_image}-amd64", + "--arch", + "amd64", + ] + ) + # if creation and annotation worked, push the new manifest + if ( + annotate_manifest_arm.returncode is 0 + and annotate_manifest_amd.returncode is 0 + ): + # rebuilds aren't a factor now and the tags are semver sorted, so the last writer is the right one + major_manifest_name = f"gcr.io/ironcore-images/{image_name}:{major}" + minor_manifest_name = ( + f"gcr.io/ironcore-images/{image_name}:{major}.{minor}" + ) + subprocess.run( + [ + "docker", + "manifest", + "create", + major_manifest_name, + f"{gcr_image}-arm64", + f"{gcr_image}-amd64", + ] + ) + subprocess.run( + [ + "docker", + "manifest", + "create", + minor_manifest_name, + f"{gcr_image}-arm64", + f"{gcr_image}-amd64", + ] + ) + subprocess.run(["docker", "manifest", "push", gcr_image]) + subprocess.run(["docker", "manifest", "push", major_manifest_name]) + subprocess.run(["docker", "manifest", "push", minor_manifest_name]) From e80167dd1ba9a9fd3f09fbeb63d9228c85aaa7e8 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Mon, 5 Feb 2024 16:34:17 -0700 Subject: [PATCH 2/3] Add README for tag state --- tag-state/README.md | 15 +++++++++++++++ tag-state/calculate-tag-state.py | 1 + 2 files changed, 16 insertions(+) create mode 100644 tag-state/README.md diff --git a/tag-state/README.md b/tag-state/README.md new file mode 100644 index 00000000..9bf65680 --- /dev/null +++ b/tag-state/README.md @@ -0,0 +1,15 @@ +# Buildlog Tag State + +This script is used to repair IronCore's Google Container Registry in case tags ever get out of sync with the buildlog. GCR is deprecated but this same script could be modified to move from GCR to GAR (or to clone IronCore's public images with tags into a local repository based on the public buildlog). + +## Usage + +Requires the `docker` CLI tool to be installed and working. If pushing to IronCore's GCR, you must be correctly authenticated (use `icl-auth`). + +```console +./fix-tag-state.py ../tenant-security-proxy.json +``` + +The script will use the buildlog to print out the correct state of images and their tags. It'll then pull the image digests referenced by that state and push that state out into GCR. + +> WARNING: there's currently no dry run functionality, if you run it and don't kill it when it first starts running docker commands, things will be changed. diff --git a/tag-state/calculate-tag-state.py b/tag-state/calculate-tag-state.py index 00b5c75d..49d47007 100755 --- a/tag-state/calculate-tag-state.py +++ b/tag-state/calculate-tag-state.py @@ -273,6 +273,7 @@ def pretty_printable_dict(d: dict) -> str: f"{gcr_image}-amd64", ] ) + # the more specific annotation of the first manifest seems to carry through to the others subprocess.run(["docker", "manifest", "push", gcr_image]) subprocess.run(["docker", "manifest", "push", major_manifest_name]) subprocess.run(["docker", "manifest", "push", minor_manifest_name]) From fe00735f9ef7da0672ea4074307af094305acfa8 Mon Sep 17 00:00:00 2001 From: Murph Murphy Date: Tue, 6 Feb 2024 14:21:48 -0700 Subject: [PATCH 3/3] Update tag-state/calculate-tag-state.py Co-authored-by: Chris Jones --- tag-state/calculate-tag-state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tag-state/calculate-tag-state.py b/tag-state/calculate-tag-state.py index 49d47007..d95f1241 100755 --- a/tag-state/calculate-tag-state.py +++ b/tag-state/calculate-tag-state.py @@ -132,7 +132,7 @@ def build_tag_state(buildlog_data: list) -> dict: tag_state[manifest_semver] = {"digest": digest, "status": PENDING} else: tag_state[major] = {"digest": digest, "status": PENDING} - tag_state[f"{major}.{minor}"] = { + tag_state[medium_tag] = { "digest": digest, "status": PENDING, }