diff --git a/.gitignore b/.gitignore index 7577e73..12ce279 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /multiarch/ .vscode/ +__pycache__/ diff --git a/scripts/common.py b/scripts/common.py new file mode 100644 index 0000000..2b47998 --- /dev/null +++ b/scripts/common.py @@ -0,0 +1,81 @@ +import os + +import dxf + +MTYPE_PLUGIN_CONFIG = 'application/vnd.docker.plugin.v1+json' +MTYPE_LAYER = 'application/vnd.docker.image.rootfs.diff.tar.gzip' +MTYPE_MANIFEST = 'application/vnd.docker.distribution.manifest.v2+json' +MTYPE_MANIFEST_LIST = 'application/vnd.docker.distribution.manifest.list.v2+json' + +class Platform: + def __init__(self, s): + self.buildx = s + + split = s.split('/') + if len(split) < 2: + raise Exception('Invalid platform format') + + self.dirname = '_'.join(split) + + self.os = split[0] + self.architecture = split[1] + + self.variant = None + if len(split) > 3: + raise Exception('Invalid platform format') + elif len(split) == 3: + self.variant = split[2] + elif self.architecture == 'arm64': + # Weird exception? (seen in alpine images) + self.variant = 'v8' + + @property + def manifest(self): + d = { + 'os': self.os, + 'architecture': self.architecture, + } + if self.variant is not None: + d['variant'] = self.variant + + return d + + def tag(self, t): + if self.variant is not None: + return f'{t}-{self.os}-{self.architecture}-{self.variant}' + return f'{t}-{self.os}-{self.architecture}' + + def __str__(self): + return f'Platform(os={self.os}, architecture={self.architecture}, variant={self.variant})' + def __repr__(self): + return str(self) + +def dxf_auth(reg: dxf.DXF, res): + reg.authenticate(username=os.getenv('REGISTRY_USERNAME'), password=os.getenv('REGISTRY_PASSWORD'), response=res) + +class DXF(dxf.DXF): + def set_manifest(self, alias, manifest_json, mime=MTYPE_MANIFEST): + """ + Give a name (alias) to a manifest. + + :param alias: Alias name + :type alias: str + + :param manifest_json: A V2 Schema 2 manifest JSON string + :type digests: list + """ + self._request('put', + 'manifests/' + alias, + data=manifest_json, + headers={'Content-Type': mime}) + + def push_manifest(self, manifest_dict, ref=None, mime=MTYPE_MANIFEST): + mf = json.dumps(manifest_dict, sort_keys=True).encode('utf-8') + size = len(mf) + digest = dxf.hash_bytes(mf) + + if ref is None: + ref = digest + + self.set_manifest(ref, mf, mime=mime) + return size, digest diff --git a/scripts/push_multiarch_plugin.py b/scripts/push_multiarch_plugin.py index 0d1be89..12251da 100755 --- a/scripts/push_multiarch_plugin.py +++ b/scripts/push_multiarch_plugin.py @@ -8,87 +8,9 @@ import gzip import tarfile import concurrent.futures -import requests from docker_image import reference -import dxf - -MTYPE_PLUGIN_CONFIG = 'application/vnd.docker.plugin.v1+json' -MTYPE_LAYER = 'application/vnd.docker.image.rootfs.diff.tar.gzip' -MTYPE_MANIFEST = 'application/vnd.docker.distribution.manifest.v2+json' -MTYPE_MANIFEST_LIST = 'application/vnd.docker.distribution.manifest.list.v2+json' - -class Platform: - def __init__(self, s): - self.buildx = s - - split = s.split('/') - if len(split) < 2: - raise Exception('Invalid platform format') - - self.dirname = '_'.join(split) - - self.os = split[0] - self.architecture = split[1] - - self.variant = None - if len(split) > 3: - raise Exception('Invalid platform format') - elif len(split) == 3: - self.variant = split[2] - elif self.architecture == 'arm64': - # Weird exception? (seen in alpine images) - self.variant = 'v8' - - @property - def manifest(self): - d = { - 'os': self.os, - 'architecture': self.architecture, - } - if self.variant is not None: - d['variant'] = self.variant - - return d - @property - def tag(self): - if self.variant is not None: - return f'{self.os}-{self.architecture}-{self.variant}' - return f'{self.os}-{self.architecture}' - - def __str__(self): - return f'Platform(os={self.os}, architecture={self.architecture}, variant={self.variant})' - def __repr__(self): - return str(self) - -def dxf_auth(reg: dxf.DXF, res): - reg.authenticate(username=os.getenv('REGISTRY_USERNAME'), password=os.getenv('REGISTRY_PASSWORD'), response=res) - -class DXF(dxf.DXF): - def set_manifest(self, alias, manifest_json, mime=MTYPE_MANIFEST): - """ - Give a name (alias) to a manifest. - - :param alias: Alias name - :type alias: str - - :param manifest_json: A V2 Schema 2 manifest JSON string - :type digests: list - """ - self._request('put', - 'manifests/' + alias, - data=manifest_json, - headers={'Content-Type': mime}) - - def push_manifest(self, manifest_dict, ref=None, mime=MTYPE_MANIFEST): - mf = json.dumps(manifest_dict, sort_keys=True).encode('utf-8') - size = len(mf) - digest = dxf.hash_bytes(mf) - - if ref is None: - ref = digest - - self.set_manifest(ref, mf, mime=mime) - return size, digest + +from common import * def tar_filter(p: Platform): def f(info: tarfile.TarInfo): @@ -120,7 +42,6 @@ def main(): tag = ref['tag'] reg = DXF(hostname, repo, auth=dxf_auth) - #print(reg.list_aliases()) print(f'Pushing config file `{args.config}`') config_size = os.path.getsize(args.config) @@ -151,7 +72,7 @@ def main(): layer_digest = reg.push_blob(data=f, digest=h, check_exists=True) print(f'Pushed {p.buildx} layer as {layer_digest}') - platform_tag = f'{tag}-{p.tag}' + platform_tag = p.tag(tag) print(f'Pushing {p.buildx} manifest with tag {platform_tag}') size, digest = reg.push_manifest({ 'schemaVersion': 2, diff --git a/scripts/tag_multiarch_plugin.py b/scripts/tag_multiarch_plugin.py new file mode 100755 index 0000000..c4799bf --- /dev/null +++ b/scripts/tag_multiarch_plugin.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import argparse + +from docker_image import reference + +from common import * + +def main(): + parser = argparse.ArgumentParser(description='Re-tag an existing multiarch plugin') + parser.add_argument('image', help='existing image (registry/image:tag)') + parser.add_argument('tag', help='new tag') + parser.add_argument('-p', '--platforms', default='linux/amd64', help='buildx platforms') + + args = parser.parse_args() + + platforms = [Platform(p) for p in args.platforms.split(',')] + + ref = reference.Reference.parse(args.image) + hostname, repo = ref.split_hostname() + without_tag = ref['name'] + + old_tag = ref['tag'] + new_tag = args.tag + + reg = DXF(hostname, repo, auth=dxf_auth) + + for p in platforms: + mf = reg.get_manifest(p.tag(old_tag)) + + print(f'Re-tagging {without_tag}:{p.tag(old_tag)} as {without_tag}:{p.tag(new_tag)}') + reg.set_manifest(p.tag(new_tag), mf, mime=MTYPE_MANIFEST) + + print(f'Re-tagging {args.image} as {without_tag}:{new_tag}') + mf = reg.get_manifest(old_tag) + reg.set_manifest(new_tag, mf, mime=MTYPE_MANIFEST_LIST) + +if __name__ == '__main__': + main()