Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package openSUSE-release-tools for 
openSUSE:Factory checked in at 2022-05-31 17:38:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/openSUSE-release-tools (Old)
 and      /work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.1548 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "openSUSE-release-tools"

Tue May 31 17:38:09 2022 rev:434 rq:980103 version:20220531.7e00d7d8

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/openSUSE-release-tools/openSUSE-release-tools.changes
    2022-05-31 15:49:01.968053969 +0200
+++ 
/work/SRC/openSUSE:Factory/.openSUSE-release-tools.new.1548/openSUSE-release-tools.changes
  2022-05-31 17:38:17.927028625 +0200
@@ -1,0 +2,6 @@
+Tue May 31 14:06:09 UTC 2022 - opensuse-releaset...@opensuse.org
+
+- Update to version 20220531.7e00d7d8:
+  * Introduce a new docker-publisher bot
+
+-------------------------------------------------------------------

Old:
----
  openSUSE-release-tools-20220531.932157b8.obscpio

New:
----
  openSUSE-release-tools-20220531.7e00d7d8.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ openSUSE-release-tools.spec ++++++
--- /var/tmp/diff_new_pack.iYTSFT/_old  2022-05-31 17:38:18.563028945 +0200
+++ /var/tmp/diff_new_pack.iYTSFT/_new  2022-05-31 17:38:18.567028947 +0200
@@ -20,7 +20,7 @@
 %define source_dir openSUSE-release-tools
 %define announcer_filename factory-package-news
 Name:           openSUSE-release-tools
-Version:        20220531.932157b8
+Version:        20220531.7e00d7d8
 Release:        0
 Summary:        Tools to aid in staging and release work for openSUSE/SUSE
 License:        GPL-2.0-or-later AND MIT
@@ -113,6 +113,18 @@
 %description check-source
 Check source review bot that performs basic source analysis and assigns 
reviews.
 
+%package docker-publisher
+Summary:        Docker image publishing bot
+Group:          Development/Tools/Other
+BuildArch:      noarch
+Requires:       python3-lxml
+Requires:       python3-requests
+Requires(pre):  shadow
+
+%description docker-publisher
+A docker image publishing bot which regularly pushes built docker images from
+several sources (Repo, URL) to several destinations (git, Docker registries)
+
 %package maintenance
 Summary:        Maintenance related services
 Group:          Development/Tools/Other
@@ -301,6 +313,14 @@
 %postun check-source
 %{systemd_postun}
 
+%pre docker-publisher
+getent passwd osrt-docker-publisher > /dev/null || \
+  useradd -r -m -s /sbin/nologin -c "user for 
openSUSE-release-tools-docker-publisher" osrt-docker-publisher
+exit 0
+
+%postun docker-publisher
+%{systemd_postun}
+
 %pre maintenance
 getent passwd osrt-maintenance > /dev/null || \
   useradd -r -m -s /sbin/nologin -c "user for 
openSUSE-release-tools-maintenance" osrt-maintenance
@@ -372,6 +392,8 @@
 %exclude %{_datadir}/%{source_dir}/check_maintenance_incidents.py
 %exclude %{_datadir}/%{source_dir}/check_source.py
 %exclude %{_datadir}/%{source_dir}/devel-project.py
+%exclude %{_datadir}/%{source_dir}/docker_publisher.py
+%exclude %{_datadir}/%{source_dir}/docker_registry.py
 %exclude %{_datadir}/%{source_dir}/metrics
 %exclude %{_datadir}/%{source_dir}/metrics.py
 %exclude %{_datadir}/%{source_dir}/metrics_release.py
@@ -409,6 +431,13 @@
 %{_bindir}/osrt-check_source
 %{_datadir}/%{source_dir}/check_source.py
 
+%files docker-publisher
+%{_bindir}/osrt-docker_publisher
+%{_datadir}/%{source_dir}/docker_publisher.py
+%{_datadir}/%{source_dir}/docker_registry.py
+%{_unitdir}/osrt-docker-publisher.service
+%{_unitdir}/osrt-docker-publisher.timer
+
 %files maintenance
 %{_bindir}/osrt-check_maintenance_incidents
 %{_datadir}/%{source_dir}/check_maintenance_incidents.py

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.iYTSFT/_old  2022-05-31 17:38:18.611028969 +0200
+++ /var/tmp/diff_new_pack.iYTSFT/_new  2022-05-31 17:38:18.615028971 +0200
@@ -1,7 +1,7 @@
 <servicedata>
   <service name="tar_scm">
     <param 
name="url">https://github.com/openSUSE/openSUSE-release-tools.git</param>
-    <param 
name="changesrevision">ba3b4174aa37bddbca58f6887913c86bf14ed67b</param>
+    <param 
name="changesrevision">7e00d7d8cbc711305dcee3e12918d148c1173fec</param>
   </service>
 </servicedata>
 

++++++ openSUSE-release-tools-20220531.932157b8.obscpio -> 
openSUSE-release-tools-20220531.7e00d7d8.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/openSUSE-release-tools-20220531.932157b8/.noserc 
new/openSUSE-release-tools-20220531.7e00d7d8/.noserc
--- old/openSUSE-release-tools-20220531.932157b8/.noserc        2022-05-31 
14:59:40.000000000 +0200
+++ new/openSUSE-release-tools-20220531.7e00d7d8/.noserc        2022-05-31 
16:04:46.000000000 +0200
@@ -1,2 +1,3 @@
 [nosetests]
 ignore-files=metrics_release\.py
+ignore-files=docker_.+\.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20220531.932157b8/dist/package/openSUSE-release-tools.spec
 
new/openSUSE-release-tools-20220531.7e00d7d8/dist/package/openSUSE-release-tools.spec
--- 
old/openSUSE-release-tools-20220531.932157b8/dist/package/openSUSE-release-tools.spec
       2022-05-31 14:59:40.000000000 +0200
+++ 
new/openSUSE-release-tools-20220531.7e00d7d8/dist/package/openSUSE-release-tools.spec
       2022-05-31 16:04:46.000000000 +0200
@@ -113,6 +113,18 @@
 %description check-source
 Check source review bot that performs basic source analysis and assigns 
reviews.
 
+%package docker-publisher
+Summary:        Docker image publishing bot
+Group:          Development/Tools/Other
+BuildArch:      noarch
+Requires:       python3-requests
+Requires:       python3-lxml
+Requires(pre):  shadow
+
+%description docker-publisher
+A docker image publishing bot which regularly pushes built docker images from
+several sources (Repo, URL) to several destinations (git, Docker registries)
+
 %package maintenance
 Summary:        Maintenance related services
 Group:          Development/Tools/Other
@@ -301,6 +313,14 @@
 %postun check-source
 %{systemd_postun}
 
+%pre docker-publisher
+getent passwd osrt-docker-publisher > /dev/null || \
+  useradd -r -m -s /sbin/nologin -c "user for 
openSUSE-release-tools-docker-publisher" osrt-docker-publisher
+exit 0
+
+%postun docker-publisher
+%{systemd_postun}
+
 %pre maintenance
 getent passwd osrt-maintenance > /dev/null || \
   useradd -r -m -s /sbin/nologin -c "user for 
openSUSE-release-tools-maintenance" osrt-maintenance
@@ -372,6 +392,8 @@
 %exclude %{_datadir}/%{source_dir}/check_maintenance_incidents.py
 %exclude %{_datadir}/%{source_dir}/check_source.py
 %exclude %{_datadir}/%{source_dir}/devel-project.py
+%exclude %{_datadir}/%{source_dir}/docker_publisher.py
+%exclude %{_datadir}/%{source_dir}/docker_registry.py
 %exclude %{_datadir}/%{source_dir}/metrics
 %exclude %{_datadir}/%{source_dir}/metrics.py
 %exclude %{_datadir}/%{source_dir}/metrics_release.py
@@ -409,6 +431,13 @@
 %{_bindir}/osrt-check_source
 %{_datadir}/%{source_dir}/check_source.py
 
+%files docker-publisher
+%{_bindir}/osrt-docker_publisher
+%{_datadir}/%{source_dir}/docker_publisher.py
+%{_datadir}/%{source_dir}/docker_registry.py
+%{_unitdir}/osrt-docker-publisher.service
+%{_unitdir}/osrt-docker-publisher.timer
+
 %files maintenance
 %{_bindir}/osrt-check_maintenance_incidents
 %{_datadir}/%{source_dir}/check_maintenance_incidents.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20220531.932157b8/docker_publisher.py 
new/openSUSE-release-tools-20220531.7e00d7d8/docker_publisher.py
--- old/openSUSE-release-tools-20220531.932157b8/docker_publisher.py    
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20220531.7e00d7d8/docker_publisher.py    
2022-05-31 16:04:46.000000000 +0200
@@ -0,0 +1,475 @@
+#!/usr/bin/python3
+#
+# Copyright (c) 2022 SUSE LLC
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# This script's job is to listen for new releases of products with docker 
images
+# and publish those.
+
+import argparse
+import json
+import os
+import re
+import requests
+import subprocess
+import sys
+import tempfile
+from lxml import etree as xml
+
+import docker_registry
+
+REPOMD_NAMESPACES = {'md': "http://linux.duke.edu/metadata/common";,
+                     'repo': "http://linux.duke.edu/metadata/repo";,
+                     'rpm': "http://linux.duke.edu/metadata/rpm"}
+
+
+class DockerImagePublisher:
+    """Base class for handling the publishing of docker images.
+    This handles multiple architectures, which have different layers
+    and therefore versions."""
+
+    def releasedDockerImageVersion(self, arch):
+        """This function returns an identifier for the released docker
+        image's version."""
+        raise Exception("pure virtual")
+
+    def prepareReleasing(self):
+        """Prepare the environment to allow calls to releaseDockerImage."""
+        raise Exception("pure virtual")
+
+    def addImage(self, version, arch, image_path):
+        """This function adds the docker image with the image manifest, config 
layers
+        in image_path."""
+        raise Exception("pure virtual")
+
+    def finishReleasing(self):
+        """This function publishes the released layers."""
+        raise Exception("pure virtual")
+
+
+class DockerPublishException(Exception):
+    pass
+
+
+class DockerImageFetcher:
+    """Base class for handling the acquiring of docker images."""
+
+    def currentVersion(self):
+        """This function returns the version of the latest available version
+        of the image for the product."""
+        raise Exception("pure virtual")
+
+    def getDockerImage(self, callback):
+        """This function downloads the root fs layer and calls callback
+        with its path as argument."""
+        raise Exception("pure virtual")
+
+
+class DockerFetchException(Exception):
+    pass
+
+
+class DockerImagePublisherRegistry(DockerImagePublisher):
+    """The DockerImagePublisherRegistry class works by using a manifest list to
+    describe a tag. The list contains a manifest for each architecture.
+    The manifest will be edited instead of replaced, which means if you don't
+    call addImage for an architecture, the existing released image stays in 
place."""
+    MAP_ARCH_RPM_DOCKER = {'i586': ("386", None),
+                           'x86_64': ("amd64", None),
+                           'armv6l': ("arm", "v6"),
+                           'armv7l': ("arm", "v7"),
+                           'aarch64': ("arm64", "v8"),
+                           'ppc64le': ("ppc64le", None),
+                           's390x': ("s390x", None)}
+
+    def __init__(self, dhc, tag, aliases=[]):
+        """Construct a DIPR by passing a DockerRegistryClient instance as dhc
+        and a name for a tag as tag.
+        Optionally, add tag aliases as aliases. Those will only be written to,
+        never read."""
+        self.dhc = dhc
+        self.tag = tag
+        self.aliases = aliases
+        # The manifestlist for the tag is only downloaded if this cache is 
empty,
+        # so needs to be set to None to force a redownload.
+        self.cached_manifestlist = None
+        # Construct a new manifestlist for the tag.
+        self.new_manifestlist = None
+
+    def getDockerArch(self, arch):
+        if arch not in self.MAP_ARCH_RPM_DOCKER:
+            raise DockerPublishException("Unknown arch %s" % arch)
+
+        return self.MAP_ARCH_RPM_DOCKER[arch]
+
+    def _getManifestlist(self):
+        if self.cached_manifestlist is None:
+            self.cached_manifestlist = self.dhc.getManifest(self.tag)
+
+        return self.cached_manifestlist
+
+    def releasedDockerImageVersion(self, arch):
+        docker_arch, docker_variant = self.getDockerArch(arch)
+
+        manifestlist = self._getManifestlist()
+
+        if manifestlist is None:
+            # No manifest -> force outdated version
+            return "0"
+
+        for manifest in manifestlist['manifests']:
+            if docker_variant is not None:
+                if 'variant' not in manifest['platform'] or 
manifest['platform']['variant'] != docker_variant:
+                    continue
+
+            if manifest['platform']['architecture'] == docker_arch:
+                if 'vnd-opensuse-version' in manifest:
+                    return manifest['vnd-opensuse-version']
+
+        # Arch not in the manifest -> force outdated version
+        return "0"
+
+    def prepareReleasing(self):
+        if self.new_manifestlist is not None:
+            raise DockerPublishException("Did not finish publishing")
+
+        self.new_manifestlist = self._getManifestlist()
+
+        # Generate an empty manifestlist
+        if not self.new_manifestlist:
+            self.new_manifestlist = {'schemaVersion': 2,
+                                     'tag': self.tag,
+                                     'mediaType': 
"application/vnd.docker.distribution.manifest.list.v2+json",
+                                     'manifests': []}
+
+        return True
+
+    def getV2ManifestEntry(self, path, filename, mediaType):
+        """For V1 -> V2 schema conversion. filename has to contain the 
digest"""
+        digest = filename
+
+        if re.match(r"^[a-f0-9]{64}", digest):
+            digest = "sha256:" + os.path.splitext(digest)[0]
+
+        if not digest.startswith("sha256"):
+            raise DockerPublishException("Invalid manifest contents")
+
+        return {'mediaType': mediaType,
+                'size': os.path.getsize(path + "/" + filename),
+                'digest': digest,
+                'x-osdp-filename': filename}
+
+    def convertV1ToV2Manifest(self, path, manifest_v1):
+        """Converts the v1 manifest in manifest_v1 to a V2 manifest and 
returns it"""
+
+        layers = []
+        # The order of layers changed in V1 -> V2
+        for layer_filename in manifest_v1['Layers'][::-1]:
+            layers += [self.getV2ManifestEntry(path, layer_filename,
+                                               
"application/vnd.docker.image.rootfs.diff.tar.gzip")]
+
+        return {'schemaVersion': 2,
+                'mediaType': 
"application/vnd.docker.distribution.manifest.v2+json",
+                'config': self.getV2ManifestEntry(path, manifest_v1['Config'],
+                                                  
"application/vnd.docker.container.image.v1+json"),
+                'layers': layers}
+
+    def addImage(self, version, arch, image_path):
+        docker_arch, docker_variant = self.getDockerArch(arch)
+
+        manifest = None
+
+        with open(image_path + "/manifest.json") as manifest_file:
+            manifest = json.load(manifest_file)
+
+        manifest_v2 = self.convertV1ToV2Manifest(image_path, manifest[0])
+        # Upload blobs
+        if not self.dhc.uploadBlob(image_path + "/" + 
manifest_v2['config']['x-osdp-filename'],
+                                   manifest_v2['config']['digest']):
+            raise DockerPublishException("Could not upload the image config")
+
+        for layer in manifest_v2['layers']:
+            if not self.dhc.uploadBlob(image_path + "/" + 
layer['x-osdp-filename'],
+                                       layer['digest']):
+                raise DockerPublishException("Could not upload an image layer")
+
+        # Upload the manifest
+        manifest_content = json.dumps(manifest_v2).encode("utf-8")
+        manifest_digest = self.dhc.uploadManifest(manifest_content)
+
+        if manifest_digest is False:
+            raise DockerPublishException("Could not upload the manifest")
+
+        # Register the manifest in the list
+        replaced = False
+        for manifest in self.new_manifestlist['manifests']:
+            if 'variant' in manifest['platform'] and 
manifest['platform']['variant'] != docker_variant:
+                continue
+
+            if manifest['platform']['architecture'] == docker_arch:
+                manifest['mediaType'] = manifest_v2['mediaType']
+                manifest['size'] = len(manifest_content)
+                manifest['digest'] = manifest_digest
+                manifest['vnd-opensuse-version'] = version
+                if docker_variant is not None:
+                    manifest['platform']['variant'] = docker_variant
+
+                replaced = True
+
+        if not replaced:
+            # Add it instead
+            manifest = {'mediaType': manifest_v2['mediaType'],
+                        'size': len(manifest_content),
+                        'digest': manifest_digest,
+                        'vnd-opensuse-version': version,
+                        'platform': {
+                            'architecture': docker_arch,
+                            'os': "linux"}
+                        }
+            if docker_variant is not None:
+                manifest['platform']['variant'] = docker_variant
+
+            self.new_manifestlist['manifests'] += [manifest]
+
+        return True
+
+    def finishReleasing(self):
+        # Generate the manifest content
+        manifestlist_content = 
json.dumps(self.new_manifestlist).encode('utf-8')
+
+        # Push the aliases
+        for alias in self.aliases:
+            if not self.dhc.uploadManifest(manifestlist_content, alias):
+                raise DockerPublishException("Could not push an manifest list 
alias")
+
+        # Push the new manifest list
+        if not self.dhc.uploadManifest(manifestlist_content, self.tag):
+            raise DockerPublishException("Could not upload the new manifest 
list")
+
+        self.new_manifestlist = None
+        self.cached_manifestlist = None  # force redownload
+
+        return True
+
+
+class DockerImageFetcherURL(DockerImageFetcher):
+    """A trivial implementation. It downloads a (compressed) tar archive and 
passes
+    the decompressed contents to the callback.
+    The version number can't be determined automatically (it would need to 
extract
+    the image and look at /etc/os-release each time - too expensive.) so it
+    has to be passed manually."""
+    def __init__(self, version, url):
+        self.version = version
+        self.url = url
+
+    def currentVersion(self):
+        return self.version
+
+    def getDockerImage(self, callback):
+        """Download the tar and extract it"""
+        with tempfile.NamedTemporaryFile() as tar_file:
+            tar_file.write(requests.get(self.url).content)
+            with tempfile.TemporaryDirectory() as tar_dir:
+                # Extract the .tar.xz into the dir
+                subprocess.call("tar -xaf '%s' -C '%s'" % (tar_file.name, 
tar_dir), shell=True)
+                return callback(tar_dir)
+
+
+class DockerImageFetcherOBS(DockerImageFetcher):
+    """Uses the OBS API to access the build artifacts.
+    Url has to be 
https://build.opensuse.org/public/build/<project>/<repo>/<arch>/<pkgname>
+    If maintenance_release is True, it picks the buildcontainer released last 
with that name.
+    e.g. for "foo" it would pick "foo.2019" instead of "foo" or "foo.2018"."""
+    def __init__(self, url, maintenance_release=False):
+        self.url = url
+        self.newest_release_url = None
+        if not maintenance_release:
+            self.newest_release_url = url
+
+    def _isMaintenanceReleaseOf(self, release, source):
+        """Returns whether release describes a maintenance release of source.
+        E.g. "foo.2019", "foo" -> True, "foo-asdf", "foo" -> False"""
+        sourcebuildflavor = source.split(":")[1] if ":" in source else None
+        releasebuildflavor = release.split(":")[1] if ":" in release else None
+        return sourcebuildflavor == releasebuildflavor and 
release.startswith(source.split(":")[0] + ".")
+
+    def _getNewestReleaseUrl(self):
+        if self.newest_release_url is None:
+            buildcontainername = self.url.split("/")[-1]
+            prjurl = self.url + "/.."
+            buildcontainerlist_req = requests.get(prjurl)
+            buildcontainerlist = xml.fromstring(buildcontainerlist_req.content)
+            releases = [entry for entry in 
buildcontainerlist.xpath("entry/@name") if
+                        self._isMaintenanceReleaseOf(entry, 
buildcontainername)]
+            releases.sort()
+            # Pick the first one with binaries
+            for release in releases[::-1] + [buildcontainername]:
+                self.newest_release_url = prjurl + "/" + release
+                try:
+                    self._getFilename()
+                    break
+                except DockerFetchException:
+                    continue
+
+        return self.newest_release_url
+
+    def _getFilename(self):
+        """Return the name of the binary at the URL with the filename ending in
+        .docker.tar."""
+        binarylist_req = requests.get(self._getNewestReleaseUrl())
+        binarylist = xml.fromstring(binarylist_req.content)
+        for binary in binarylist.xpath("binary/@filename"):
+            if binary.endswith(".docker.tar"):
+                return binary
+
+        raise DockerFetchException("No docker image built in the repository")
+
+    def currentVersion(self):
+        """Return {version}-?({flavor}-)Build{build} of the docker file."""
+        filename = self._getFilename()
+        # Capture everything between arch and filename suffix
+        return re.match(r'[^.]*\.[^.]+-(.*)\.docker\.tar$', filename).group(1)
+
+    def getDockerImage(self, callback):
+        """Download the tar and extract it"""
+        filename = self._getFilename()
+        with tempfile.NamedTemporaryFile() as tar_file:
+            tar_file.write(requests.get(self.newest_release_url + "/" + 
filename).content)
+            with tempfile.TemporaryDirectory() as tar_dir:
+                # Extract the .tar into the dir
+                subprocess.call("tar -xaf '%s' -C '%s'" % (tar_file.name, 
tar_dir), shell=True)
+                return callback(tar_dir)
+
+
+def run():
+    drc_tw = docker_registry.DockerRegistryClient(os.environ['REGISTRY'], 
os.environ['REGISTRY_USER'], os.environ['REGISTRY_PASSWORD'],
+                                                  
os.environ['REGISTRY_REPO_TW'])
+    drc_leap = docker_registry.DockerRegistryClient(os.environ['REGISTRY'], 
os.environ['REGISTRY_USER'], os.environ['REGISTRY_PASSWORD'],
+                                                    
os.environ['REGISTRY_REPO_LEAP'])
+
+    config = {
+        'tumbleweed': {
+            'fetchers': {
+                'i586': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Tumbleweed/containers/i586/opensuse-tumbleweed-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'x86_64': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Tumbleweed/containers/x86_64/opensuse-tumbleweed-image:docker";,
 maintenance_release=True),   # noqa: E501
+                'aarch64': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Tumbleweed/containers/aarch64/opensuse-tumbleweed-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'armv7l': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Tumbleweed/containers/armv7l/opensuse-tumbleweed-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'armv6l': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Tumbleweed/containers/armv6l/opensuse-tumbleweed-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'ppc64le': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Tumbleweed/containers/ppc64le/opensuse-tumbleweed-image:docker";,
 maintenance_release=True),  # noqa: E501
+                's390x': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Tumbleweed/containers/s390x/opensuse-tumbleweed-image:docker";,
 maintenance_release=True),  # noqa: E501
+            },
+            'publisher': DockerImagePublisherRegistry(drc_tw, "latest"),
+        },
+        'leap-15.3': {
+            'fetchers': {
+                'x86_64': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.3/containers/x86_64/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'aarch64': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.3/containers/aarch64/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'armv7l': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.3/containers_armv7/armv7l/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'ppc64le': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.3/containers/ppc64le/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+                's390x': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.3/containers/s390x/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+            },
+            'publisher': DockerImagePublisherRegistry(drc_leap, "latest", 
["15.3", "15"]),
+        },
+        'leap-15.4': {
+            'fetchers': {
+                'x86_64': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.4/containers/x86_64/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'aarch64': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.4/containers/aarch64/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+                'ppc64le': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.4/containers/ppc64le/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+                's390x': 
DockerImageFetcherOBS(url="https://build.opensuse.org/public/build/openSUSE:Containers:Leap:15.4/containers/s390x/opensuse-leap-image:docker";,
 maintenance_release=True),  # noqa: E501
+            },
+            'publisher': DockerImagePublisherRegistry(drc_leap, "15.4"),
+        },
+    }
+
+    # Parse args after defining the config - the available distros are included
+    # in the help output
+    parser = argparse.ArgumentParser(description="Docker image publish script",
+                                     
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+    parser.add_argument("distros", metavar="distro", type=str, nargs="*",
+                        default=[key for key in config],
+                        help="Which distros to check for images to publish.")
+
+    args = parser.parse_args()
+
+    success = True
+
+    for distro in args.distros:
+        print("Handling %s" % distro)
+
+        archs_to_update = {}
+        fetchers = config[distro]['fetchers']
+        publisher = config[distro]['publisher']
+
+        for arch in fetchers:
+            print("\tArchitecture %s" % arch)
+            try:
+                current = fetchers[arch].currentVersion()
+                print("\t\tAvailable version: %s" % current)
+
+                released = publisher.releasedDockerImageVersion(arch)
+                print("\t\tReleased version: %s" % released)
+
+                if current != released:
+                    archs_to_update[arch] = current
+            except Exception as e:
+                print("\t\tException during version fetching: %s" % e)
+
+        if not archs_to_update:
+            print("\tNothing to do.")
+            continue
+
+        if not publisher.prepareReleasing():
+            print("\tCould not prepare the publishing")
+            success = False
+            continue
+
+        need_to_upload = False
+
+        for arch, version in archs_to_update.items():
+            print("\tUpdating %s image to version %s" % (arch, version))
+            try:
+                fetchers[arch].getDockerImage(lambda image_path: 
publisher.addImage(version=version,
+                                                                               
     arch=arch,
+                                                                               
     image_path=image_path))
+                need_to_upload = True
+
+            except DockerFetchException as dfe:
+                print("\t\tCould not fetch the image: %s" % dfe)
+                success = False
+                continue
+            except DockerPublishException as dpe:
+                print("\t\tCould not publish the image: %s" % dpe)
+                success = False
+                continue
+
+        # If nothing got added to the publisher, don't try to upload it.
+        # For docker hub it'll just update the "last pushed" time without any 
change
+        if not need_to_upload:
+            continue
+
+        if not publisher.finishReleasing():
+            print("\tCould not publish the image")
+            continue
+
+    return 0 if success else 1
+
+
+if __name__ == "__main__":
+    sys.exit(run())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20220531.932157b8/docker_registry.py 
new/openSUSE-release-tools-20220531.7e00d7d8/docker_registry.py
--- old/openSUSE-release-tools-20220531.932157b8/docker_registry.py     
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20220531.7e00d7d8/docker_registry.py     
2022-05-31 16:04:46.000000000 +0200
@@ -0,0 +1,214 @@
+#!/usr/bin/python3
+#
+# Copyright (c) 2018 SUSE LLC
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# This is a very basic client for the Docker Registry V2 API.
+# It exists for a single reason: All clients either:
+# - Don't work
+# - Don't support uploading
+# - Don't support multi-arch images (manifest lists)
+# and some even all three.
+
+import hashlib
+import json
+import os
+import urllib.parse
+import requests
+
+
+class DockerRegistryClient():
+    def __init__(self, url, username, password, repository):
+        self.url = url
+        self.username = username
+        self.password = password
+        self.repository = repository
+        self.scopes = ["repository:%s:pull,push,delete" % repository]
+        self.token = None
+
+    class DockerRegistryError(Exception):
+        """Some nicer display of docker registry errors"""
+        def __init__(self, errors):
+            self.errors = errors
+
+        def __str__(self):
+            ret = "Docker Registry errors:"
+            for error in self.errors:
+                ret += "\n" + str(error)
+
+            return ret
+
+    def _updateToken(self, www_authenticate):
+        bearer_parts = www_authenticate[len("Bearer "):].split(",")
+        bearer_dict = {}
+        for part in bearer_parts:
+            assignment = part.split('=')
+            bearer_dict[assignment[0]] = assignment[1].strip('"')
+
+        scope_param = "&scope=".join([""] + [urllib.parse.quote(scope) for 
scope in self.scopes])
+        response = requests.get("%s?service=%s%s" % (bearer_dict['realm'], 
bearer_dict['service'], scope_param),
+                                auth=(self.username, self.password))
+        self.token = response.json()['token']
+
+    def doHttpCall(self, method, url, **kwargs):
+        """This method wraps the requested method from the requests module to
+        add the token for authorization."""
+        try_update_token = True
+
+        # Relative to the host
+        if url.startswith("/"):
+            url = self.url + url
+
+        if "headers" not in kwargs:
+            kwargs['headers'] = {}
+
+        while True:
+            resp = None
+            if self.token is not None:
+                kwargs['headers']['Authorization'] = "Bearer " + self.token
+
+            methods = {'POST': requests.post,
+                       'GET': requests.get,
+                       'HEAD': requests.head,
+                       'PUT': requests.put,
+                       'DELETE': requests.delete}
+
+            if method not in methods:
+                return False
+
+            resp = methods[method](url, **kwargs)
+
+            if resp.status_code == 401 or resp.status_code == 403:
+                if try_update_token:
+                    try_update_token = False
+                    self._updateToken(resp.headers['Www-Authenticate'])
+                    continue
+
+            if resp.status_code > 400 and resp.status_code < 404:
+                try:
+                    errors = resp.json()['errors']
+                    raise self.DockerRegistryError(errors)
+                except ValueError:
+                    pass
+
+            return resp
+
+    def uploadManifest(self, content, reference=None):
+        """Upload a manifest. Data is given as bytes in content, the 
digest/tag in reference.
+        If reference is None, the digest is computed and used as reference.
+        On success, the used reference is returned. False otherwise."""
+        content_json = json.loads(content.decode('utf-8'))
+        if "mediaType" not in content_json:
+            raise Exception("Invalid manifest")
+
+        if reference is None:
+            alg = hashlib.sha256()
+            alg.update(content)
+            reference = "sha256:" + alg.hexdigest()
+
+        resp = self.doHttpCall("PUT", "/v2/%s/manifests/%s" % 
(self.repository, reference),
+                               headers={'Content-Type': 
content_json['mediaType']},
+                               data=content)
+
+        if resp.status_code != 201:
+            return False
+
+        return reference
+
+    def uploadManifestFile(self, filename, reference=None):
+        """Upload a manifest. If the filename doesn't equal the digest, it's 
computed.
+        If reference is None, the digest is used. You can use the manifest's 
tag
+        for example.
+        On success, the used reference is returned. False otherwise."""
+        with open(filename, "rb") as manifest:
+            content = manifest.read()
+
+            if reference is None:
+                basename = os.path.basename(filename)
+                if basename.startswith("sha256:"):
+                    reference = basename
+
+            if reference is None:
+                raise Exception("No reference determined")
+
+            return self.uploadManifest(content, reference)
+
+    def getManifest(self, reference):
+        """Get a (json-parsed) manifest with the given reference (digest or 
tag).
+        If the manifest does not exist, return None. For other errors, 
False."""
+        resp = self.doHttpCall("GET", "/v2/%s/manifests/%s" % 
(self.repository, reference),
+                               headers={'Accept': 
"application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json"})
  # noqa: E501
+
+        if resp.status_code == 404:
+            return None
+
+        if resp.status_code != 200:
+            return False
+
+        return resp.json()
+
+    def getManifestDigest(self, reference):
+        """Return the digest of the manifest with the given reference.
+        If the manifest doesn't exist or the request fails, it returns 
False."""
+        resp = self.doHttpCall("HEAD", "/v2/%s/manifests/%s" % 
(self.repository, reference),
+                               headers={'Accept': 
"application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.docker.distribution.manifest.v2+json"})
  # noqa: E501
+
+        if resp.status_code != 200:
+            return False
+
+        return resp.headers['Docker-Content-Digest']
+
+    def deleteManifest(self, digest):
+        """Delete the manifest with the given reference."""
+        resp = self.doHttpCall("DELETE", "/v2/%s/manifests/%s" % 
(self.repository, digest))
+
+        return resp.status_code == 202
+
+    def uploadBlob(self, filename, digest=None):
+        """Upload the blob with the given filename and digest. If digest is 
None,
+        the basename has to equal the digest.
+        Returns True if blob already exists or upload succeeded."""
+
+        if digest is None:
+            digest = os.path.basename(filename)
+
+        if not digest.startswith("sha256:"):
+            raise Exception("Invalid digest")
+
+        # Check whether the blob already exists - don't upload it needlessly.
+        stat_request = self.doHttpCall("HEAD", "/v2/%s/blobs/%s" % 
(self.repository, digest))
+        if stat_request.status_code == 200 or stat_request.status_code == 307:
+            return True
+
+        # For now we can do a single upload call with everything inlined
+        # (which also means completely in ram, but currently it's never > 50 
MiB)
+        content = None
+        with open(filename, "rb") as blob:
+            content = blob.read()
+
+        # First request an upload "slot", we get an URL we can PUT to back
+        upload_request = self.doHttpCall("POST", "/v2/%s/blobs/uploads/" % 
self.repository)
+        if upload_request.status_code == 202:
+            location = upload_request.headers['Location']
+            upload = self.doHttpCall("PUT", location + "&digest=" + digest,
+                                     data=content)
+            return upload.status_code == 201
+
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20220531.932157b8/gocd/dockerhub-publisher.yaml 
new/openSUSE-release-tools-20220531.7e00d7d8/gocd/dockerhub-publisher.yaml
--- old/openSUSE-release-tools-20220531.932157b8/gocd/dockerhub-publisher.yaml  
1970-01-01 01:00:00.000000000 +0100
+++ new/openSUSE-release-tools-20220531.7e00d7d8/gocd/dockerhub-publisher.yaml  
2022-05-31 16:04:46.000000000 +0200
@@ -0,0 +1,26 @@
+format_version: 3
+pipelines:
+  openSUSE.DockerHub.Publish:
+    group: openSUSE.Checkers
+    lock_behavior: unlockWhenFinished
+    environment_variables:
+      REGISTRY: 'https://registry-1.docker.io'
+      REGISTRY_USER: 'opensusereleasebot'
+      REGISTRY_PASSWORD: '{{SECRET:[opensuse.secrets][REGISTRY_PASSWORD]}}'
+      REGISTRY_REPO_TW: 'opensuse/tumbleweed'
+      REGISTRY_REPO_LEAP: 'opensuse/leap'
+    materials:
+      git:
+        git: https://github.com/Vogtinator/opensuse-release-tools.git
+        branch: docker-release-gocd
+    timer:
+      spec: 0 */15 * ? * *
+      only_on_changes: false
+    stages:
+    - Run:
+        approval: manual
+        resources:
+        - staging-bot
+        tasks:
+        - script:
+            ./docker_publisher.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20220531.932157b8/systemd/osrt-docker-publisher.service
 
new/openSUSE-release-tools-20220531.7e00d7d8/systemd/osrt-docker-publisher.service
--- 
old/openSUSE-release-tools-20220531.932157b8/systemd/osrt-docker-publisher.service
  1970-01-01 01:00:00.000000000 +0100
+++ 
new/openSUSE-release-tools-20220531.7e00d7d8/systemd/osrt-docker-publisher.service
  2022-05-31 16:04:46.000000000 +0200
@@ -0,0 +1,10 @@
+[Unit]
+Description=openSUSE Release Tools: Docker image publisher
+
+[Service]
+User=osrt-docker-publisher
+EnvironmentFile=/home/osrt-docker-publisher/.config/osrt-docker_publisher
+ExecStart=/usr/bin/osrt-docker_publisher
+
+[Install]
+WantedBy=multi-user.target
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/openSUSE-release-tools-20220531.932157b8/systemd/osrt-docker-publisher.timer
 
new/openSUSE-release-tools-20220531.7e00d7d8/systemd/osrt-docker-publisher.timer
--- 
old/openSUSE-release-tools-20220531.932157b8/systemd/osrt-docker-publisher.timer
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/openSUSE-release-tools-20220531.7e00d7d8/systemd/osrt-docker-publisher.timer
    2022-05-31 16:04:46.000000000 +0200
@@ -0,0 +1,10 @@
+[Unit]
+Description=openSUSE Release Tools: Docker image publisher
+
+[Timer]
+OnBootSec=120
+OnUnitInactiveSec=15 min
+Unit=osrt-docker-publisher.service
+
+[Install]
+WantedBy=timers.target

++++++ openSUSE-release-tools.obsinfo ++++++
--- /var/tmp/diff_new_pack.iYTSFT/_old  2022-05-31 17:38:19.323029327 +0200
+++ /var/tmp/diff_new_pack.iYTSFT/_new  2022-05-31 17:38:19.323029327 +0200
@@ -1,5 +1,5 @@
 name: openSUSE-release-tools
-version: 20220531.932157b8
-mtime: 1654001980
-commit: 932157b819255f884b3c1638ac5a8468385924f6
+version: 20220531.7e00d7d8
+mtime: 1654005886
+commit: 7e00d7d8cbc711305dcee3e12918d148c1173fec
 

Reply via email to