From e6e6f931c120232f41e8bc3c9869d7e311843411 Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Fri, 28 Sep 2018 16:00:31 +0100
Subject: [PATCH] Refactor code to wait for builds recursively

This ensures images are built in the right dependency order.
---
 gitlab_docker_autodep/main.py | 141 ++++++++++++++++++----------------
 1 file changed, 75 insertions(+), 66 deletions(-)

diff --git a/gitlab_docker_autodep/main.py b/gitlab_docker_autodep/main.py
index f55cbee..411b56f 100644
--- a/gitlab_docker_autodep/main.py
+++ b/gitlab_docker_autodep/main.py
@@ -1,17 +1,18 @@
 import gitlab
 import optparse
-import re
+import os
 import sys
+import time
 import urlparse
 
 
-def parse_dockerfile(df):
+def _parse_dockerfile(df):
     for line in df.split('\n'):
         if line.startswith('FROM '):
             return line[5:].strip()
 
 
-def fetch_dockerfile(gl, project):
+def _fetch_dockerfile(gl, project):
     try:
         f = project.files.get(file_path='Dockerfile', ref='master')
         return f.decode()
@@ -19,97 +20,105 @@ def fetch_dockerfile(gl, project):
         return None
 
 
-def repo_path_from_image(image_name, registry):
-    if not image_name.startswith(registry):
-        return None
-    repo = image_name[len(registry)+1:]
-    repo = repo.split(':')[0]
-    return repo
-
-
-def build_dependency_tree(gl, registry, match_pattern):
-    """Build a dependency map of base images for all projects.
+def _remove_image_tag(name):
+    if ':' in name:
+        return name.split(':')[0]
+    return name
 
-    Only includes projects matching the given regexp, having a valid
-    Dockerfile, and whose base image is hosted on the specified Docker
-    registry (usually the Gitlab-hosted one).
 
-    """
-    dtree = {}
-    match_rx = re.compile(match_pattern)
+def build_dependency_tree(gl, search_pattern=None):
+    """Build the project dependency map based on Dockerfiles."""
+    deps = {}
 
-    # Use a generator to scan over the full list of projects
-    # (potentially large).
-    projects = gl.projects.list(all=True, as_list=False)
+    projects = gl.projects.list(all=True, search=search_pattern, as_list=False)
     for project in projects:
-        if not match_rx.search(project.path_with_namespace):
-            continue
-        df = fetch_dockerfile(gl, project)
+        df = _fetch_dockerfile(gl, project)
         if not df:
             continue
-        base_image = parse_dockerfile(df)
+        base_image = _parse_dockerfile(df)
         if not base_image:
             print >>sys.stderr, 'ERROR: could not find base image for %s' % (
                 project.path_with_namespace,)
             continue
-        base_repo = repo_path_from_image(base_image, registry)
-        if not base_repo:
-            # External base.
-            continue
+        deps.setdefault(_remove_image_tag(base_image), []).append(project)
+    return deps
 
-        dtree.setdefault(base_repo, []).append(project)
 
-    return dtree
+def rebuild(project, wait=False):
+    pipeline = project.pipelines.create({'ref': 'master'})
+    if wait:
+        while pipeline.finished_at is None:
+            pipeline.refresh()
+            time.sleep(3)
+    return pipeline
 
 
-def find_deps(dtree, repo, recurse):
-    deps = []
-    def _add_deps(x):
-        for d in dtree.get(x, []):
-            deps.append(d)
-            if recurse:
-                _add_deps(d.path_with_namespace)
-    _add_deps(repo)
-    return deps
+def rebuild_deps(gitlab_url, registry_hostname, gitlab_token,
+                 search_pattern, image_name,
+                 dry_run=False, recurse=False, wait=False):
+    """Rebuild dependencies of the given image."""
+    gl = gitlab.Gitlab(gitlab_url, private_token=gitlab_token)
+    if gitlab_token:
+        gl.auth()
+
+    deps = build_dependency_tree(gl, search_pattern)
+
+    stack = deps.get(_remove_image_tag(image_name), [])
+    while stack:
+        project = stack.pop(0)
+
+        print 'rebuilding %s' % project.path_with_namespace
+        if not dry_run:
+            pipeline = rebuild(project, wait)
+            if pipeline.status != 'success':
+                print >>sys.stderr, 'ERROR: build failed for %s' % (
+                    project.path_with_namespace,)
+                return
+
+        if recurse:
+            image_name = '%s/%s' % (
+                registry_hostname, project.path_with_namespace)
+            stack.extend(deps.get(image_name, []))
 
 
 def main():
-    parser = optparse.OptionParser(usage='%prog [<options>] <repo_path>')
+    parser = optparse.OptionParser(usage='%prog [<options>] <image_name>')
     parser.add_option('--token', help='Authentication token')
-    parser.add_option('--registry', help='Docker registry hostname')
+    parser.add_option('--registry',
+                      help='Docker registry hostname (if empty, it will be '
+                      'automatically derived from --url)')
     parser.add_option('--url', help='Gitlab URL')
-    parser.add_option('--rebuild', action='store_true',
-                      help='Trigger a rebuild of the dependencies')
+    parser.add_option('-n', '--dry-run', action='store_true', dest='dry_run',
+                      help='Only show what would be done')
     parser.add_option('--recurse', action='store_true',
-                      help='Include all dependencies recursively')
+                      help='Include all dependencies recursively '
+                      'and wait for completion of the pipelines')
     parser.add_option('--match',
-                      default='/docker-',
-                      help='Project paths should match this regexp')
+                      help='Search keyword(s) to filter project list')
     opts, args = parser.parse_args()
 
     if not opts.url:
         parser.error('Must specify --url')
-    if len(args) > 1:
-        parser.error('Too many arguments')
+    if len(args) != 1:
+        parser.error('Bad number of arguments')
 
     # If --registry is not specified, make an educated guess.
-    registry = opts.registry
-    if not registry:
-        registry = 'registry.' + urlparse.urlsplit(opts.url).netloc
-        print >>sys.stderr, 'using %s as Docker registry' % (registry,)
-
-    gl = gitlab.Gitlab(opts.url, private_token=opts.token)
-    if opts.token:
-        gl.auth()
-
-    dtree = build_dependency_tree(gl, registry, opts.match)
-    deps = find_deps(dtree, args[0], opts.recurse)
-
-    for d in deps:
-        print d.path_with_namespace
-        if opts.rebuild:
-            d.pipelines.create({'ref': 'master'})
+    registry_hostname = opts.registry
+    if not registry_hostname:
+        registry_hostname = 'registry.' + urlparse.urlsplit(opts.url).netloc
+        print >>sys.stderr, 'using %s as Docker registry' % (registry_hostname,)
     
+    rebuild_deps(
+        opts.url,
+        registry_hostname,
+        opts.token,
+        opts.match,
+        args[0],
+        opts.dry_run,
+        opts.recurse,
+        opts.recurse,
+    )
+
 
 if __name__ == '__main__':
     main()
-- 
GitLab