Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Actually push everywhere, build manifests #5

Merged
merged 3 commits into from
Feb 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions tag-state/README.md
Original file line number Diff line number Diff line change
@@ -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.
181 changes: 168 additions & 13 deletions tag-state/calculate-tag-state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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[medium_tag] = {
"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,
)
Expand All @@ -121,4 +158,122 @@ 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",
]
)
cjyar marked this conversation as resolved.
Show resolved Hide resolved
# 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",
]
)
# 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])
Comment on lines +256 to +279
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to check the returncode on these subprocesses?

Copy link
Member Author

@skeet70 skeet70 Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"need" is a bit of a loaded term. No, since it's the last thing it'll try to do and the output will be in the same place I'd put it if it were to fail.