diff --git a/gitlab_docker_autodep/deps.py b/gitlab_docker_autodep/deps.py new file mode 100644 index 0000000000000000000000000000000000000000..531a9c218f2ca68fa235133c8ee7f777726b5aa3 --- /dev/null +++ b/gitlab_docker_autodep/deps.py @@ -0,0 +1,118 @@ +import re + + +DEFAULT_BRANCH = 'master' + + +def split_project_branch(project_with_branch): + if ':' in project_with_branch: + p, b = project_with_branch.split(':') + return p, b + return project_with_branch, DEFAULT_BRANCH + + +def list_projects(gl, search_pattern): + projects = gl.projects.list( + all=True, + search=search_pattern, + search_namespaces=True, + as_list=False, + simple=True, + ) + for p in projects: + yield p.path_with_namespace + + +def get_branches(gl, project_names): + for path_with_namespace in project_names: + p = gl.projects.get(path_with_namespace) + for b in p.branches.list(): + yield (path_with_namespace, b.name) + + +def has_ci(gl, project_path, branch_name): + p = gl.projects.get(project_path) + try: + p.files.get(file_path='.gitlab-ci.yml', ref=branch_name) + return True + except Exception: + return False + + +_from_rx = re.compile(r'^FROM\s+(\S+).*$', re.MULTILINE) + + +def get_docker_deps(gl, project_path, branch_name): + p = gl.projects.get(project_path) + try: + f = p.files.get(file_path='Dockerfile', ref=branch_name) + return _from_rx.findall(f.decode().decode('utf-8')) + except Exception: + return [] + + +def get_explicit_deps(gl, project_path, branch_name): + p = gl.projects.get(project_path) + try: + f = p.files.get(file_path='.gitlab-deps', ref=branch_name) + return f.decode().decode('utf-8').split('\n') + except Exception: + return [] + + +_docker_image_rx = re.compile(r'^([^/]*)(/([^:]*))?(:(.*))?$') + + +def docker_image_to_project(docker_image, registry_hostname): + m = _docker_image_rx.match(docker_image) + if m and m[1] == registry_hostname: + # The branch is the tag, except for 'latest' + if not m[5] or m[5] == 'latest': + branch = DEFAULT_BRANCH + else: + branch = m[5] + return m[3], branch + return None, None + + +_url_rx = re.compile(r'^(https?://[^/]+/)([^:]+)(:.*)?$') + + +def url_to_project(url, gitlab_url): + m = _url_rx.match(url) + if m and m[1] == gitlab_url: + return m[2], m[3] or DEFAULT_BRANCH + + +def not_null(l): + return filter(None, l) + + +def get_deps(gl, gitlab_url, registry_hostname, project_path, branch_name): + deps = [] + deps.extend(not_null( + url_to_project(url, gitlab_url) + for url in get_explicit_deps(gl, project_path, branch_name))) + deps.extend(not_null( + docker_image_to_project(img, registry_hostname) + for img in get_docker_deps(gl, project_path, branch_name))) + return deps + + +def list_deps(gl, gitlab_url, registry_hostname, projects): + for project_path, branch_name in projects: + deps = get_deps(gl, gitlab_url, registry_hostname, + project_path, branch_name) + for dep_path, dep_branch in deps: + print(f'{project_path}:{branch_name} {dep_path}:{dep_branch}') + + +def read_deps(fd): + deps = {} + for line in fd: + src, dst = line.strip().split() + src_project, src_branch = split_project_branch(src) + dst_project, dst_branch = split_project_branch(dst) + deps.setdefault((src_project, src_branch), []).append( + (dst_project, dst_branch)) + return deps diff --git a/gitlab_docker_autodep/main.py b/gitlab_docker_autodep/main.py index 041f2cd1b74505c9e72fea286938359840d2f77d..eb3620201a841cafce92d17957eb107960bea319 100644 --- a/gitlab_docker_autodep/main.py +++ b/gitlab_docker_autodep/main.py @@ -1,13 +1,12 @@ import argparse +import gitlab import logging import os -import time -try: - import urlparse -except ImportError: - import urllib.parse as urlparse +import sys +import urllib.parse as urlparse -from .docker_deps import dump_deps +from .deps import get_branches, list_projects, list_deps, \ + split_project_branch, read_deps from .rebuild import rebuild_deps from .server import run_app @@ -19,40 +18,51 @@ def main(): # Common options. common_parser = argparse.ArgumentParser(add_help=False) + common_parser.add_argument('--debug', action='store_true') + common_parser.add_argument( + '-n', '--dry-run', action='store_true', dest='dry_run', + help='only show what would be done') gitlab_opts_group = common_parser.add_argument_group('gitlab options') gitlab_opts_group.add_argument( - '--url', metavar='URL', help='Gitlab URL') + '--url', metavar='URL', help='Gitlab URL', + default=os.getenv('GITLAB_URL')) gitlab_opts_group.add_argument( '--token-file', metavar='FILE', type=argparse.FileType('r'), + default=os.getenv('GITLAB_AUTH_TOKEN_FILE'), help='Load Gitlab authentication token from this file') gitlab_opts_group.add_argument( '--token', metavar='TOKEN', + default=os.getenv('GITLAB_AUTH_TOKEN'), help='Gitlab authentication token') gitlab_opts_group.add_argument( '--registry', metavar='NAME', + default=os.getenv('GITLAB_REGISTRY'), help='Docker registry hostname (if empty, it will be ' 'automatically derived from --url)') - scope_opts_group = common_parser.add_argument_group('project scope options') - common_parser.add_argument('--debug', action='store_true') + + # List projects. + list_projects_parser = subparsers.add_parser( + 'list-projects', + parents=[common_parser], + help='list projects', + description='List all projects and their branches on the Gitlab ' + 'instance.') + list_projects_parser.add_argument( + '--search', + help='Search query used to filter project list on the server side') # Compute deps. deps_parser = subparsers.add_parser( 'deps', parents=[common_parser], help='build dependency map', - description='Generate a map of Docker-derived dependencies between ' - 'projects on a Gitlab instance.') - deps_parser.add_argument( - '--match', - help='Search query to filter project list on the server side') - deps_parser.add_argument( - '--filter', - help='Regexp to filter project list on the right-hand (dependency) side') + description='Generate a map of dependencies between projects on a ' + 'Gitlab instance.') deps_parser.add_argument( '--docker', action='store_true', help='Output dependencies between Docker images, not Gitlab projects') - + # Trigger rebuilds of reverse deps. rebuild_image_parser = subparsers.add_parser( 'rebuild', @@ -63,16 +73,13 @@ def main(): rebuild_image_parser.add_argument( '--deps', metavar='FILE', help='file with project dependencies') - rebuild_image_parser.add_argument( - '-n', '--dry-run', action='store_true', dest='dry_run', - help='only show what would be done') rebuild_image_parser.add_argument( '--recurse', action='store_true', help='Include all dependencies recursively ' 'and wait for completion of the pipelines') rebuild_image_parser.add_argument( 'project_path', - help='Project name (relative path)') + help='Project name (relative path, with optional branch)') # Server. server_parser = subparsers.add_parser( @@ -110,31 +117,31 @@ def main(): registry_hostname = args.registry if not registry_hostname: registry_hostname = 'registry.' + urlparse.urlsplit(args.url).netloc - logging.error('using %s as Docker registry', registry_hostname) + logging.warning('guessed %s for the Docker registry hostname', + registry_hostname) gitlab_token = args.token if not gitlab_token and args.token_file: gitlab_token = args.token_file.read().strip().encode('utf-8') + gl = gitlab.Gitlab(args.url, private_token=gitlab_token) + if gitlab_token: + gl.auth() + + # Dispatch to the command executor. + if cmd == 'list-projects': + for p, b in get_branches(gl, list_projects(gl, args.search)): + print(f'{p}:{b}') if cmd == 'deps': - dump_deps( - args.url, - registry_hostname, - gitlab_token, - args.match, - args.filter, - not args.docker, - ) + projects = [split_project_branch(x.strip()) for x in sys.stdin] + list_deps(gl, args.url, registry_hostname, projects) elif cmd == 'rebuild': - rebuild_deps( - args.url, - gitlab_token, - args.deps, - args.project_path, - args.dry_run, - args.recurse, - ) + deps = read_deps(sys.stdin) + project_path, branch_name = split_project_branch(args.project_path) + rebuild_deps(gl, deps, project_path, branch_name, args.dry_run, + args.recurse) elif cmd == 'server': + # TODO run_app( args.url, gitlab_token, diff --git a/gitlab_docker_autodep/rebuild.py b/gitlab_docker_autodep/rebuild.py index 84ac48ca2bf165ef496a46bcf3912a36ac3b2dbc..b7b70a3345efa42818b1be6c256d355157fa0dec 100644 --- a/gitlab_docker_autodep/rebuild.py +++ b/gitlab_docker_autodep/rebuild.py @@ -1,5 +1,3 @@ -import gitlab -import json import logging import time @@ -18,16 +16,9 @@ def rebuild(gl, project_path, wait=False): return pipeline -def rebuild_deps(gitlab_url, gitlab_token, - project_deps_path, project_path, dry_run, wait_and_recurse): - gl = gitlab.Gitlab(gitlab_url, private_token=gitlab_token) - if gitlab_token: - gl.auth() - - with open(project_deps_path) as fd: - project_deps = json.load(fd) - - stack = project_deps.get(project_path, []) +def rebuild_deps(gl, project_deps, project_path, branch_name, dry_run, + wait_and_recurse): + stack = project_deps.get((project_path, branch_name), []) while stack: path = stack.pop(0) logging.info('rebuilding %s', path)