"""CLI setup of docker dev environment.""" import argparse import getpass import os import subprocess from git import Repo from github import Github import keyring import yaml def main(): """Start execution.""" args = parse_args() # parse docker compose file into object compose = parse_compose(args.compose_file) services = filter_services(compose['services'], args.services) if not services: print('No services found!') exit(1) # configure each service for name, service in services.items(): print(f'=> Setting up service "{name}"') # service uses image, no need to checkout source if 'image' in service: print('skipping setup, service has "image" config') # no build or image config, that's not right! elif 'build' not in service: print('Error! service "{name}" is missing build and image config blocks') # noqa:E501 # checkout code according to build block else: build = service['build'] context = build['context'] # if code not already checked out, fork and clone if not os.path.isdir(context): repo_name = os.path.split(context)[-1] fork_and_clone( args.github_keyname, args.org, repo_name, context, args.remote_name ) # optionally update if not dirty and on trunk branch if args.update: update_from_remote( context, args.remote_name, args.remote_branch ) print() # check config, outputing any warnings subprocess.run(['docker-compose', 'config', '-q']) def auth_github(keyname): """Retrieve and setup github auth token. Args: keyname (str): name in system keyring Returns: str: value of token from keyring """ token = keyring.get_password(keyname, keyname) if not token: print() print('Access token needed for GitHub') print(' - one time setup') print(' - will be stored in your keyring as "{keyname}"') print() print('1. Visit => https://github.com/settings/tokens') print('2. Click "Generate new token"') print('3. Create token with full "repo" scope') print('4. Enter token below') token = getpass.getpass('Token here: ') keyring.set_password(keyname, keyname, token) print(f'Token stored in keyring with name "{keyname}"') return token def parse_compose(filename): """Load docker-compose from file. Args: filename (str): location of docker-compose.yml Returns: dict: parsed contents of yaml file """ with open(filename, 'r') as yamlfile: return yaml.load(yamlfile, Loader=yaml.SafeLoader) def filter_services(all_services, service_names): """Filter list of services down. Args: all_services (list): every service available service_names (list): services to explicity include Returns: dict: services mapping filtered down """ if type(service_names) == list: # include only service in explicit list services = { k: v for k, v in all_services.items() if k in service_names } # add dependencies that might be missing missing_deps = set() for name, service in services.items(): for d in service.get('depends_on', []): if d not in services: missing_deps.add(d) for missing_dep in missing_deps: if d not in all_services: print(f'Service "{d}" not found!') exit(1) services[d] = all_services[d] return services return all_services def fork_and_clone( github_keyname, org_name, repo_name, target_dir, upstream_ref): """Fork repo and clone locally. Args: github_keyname (str): identifier of token in keyring org_name (str): github account to fork from repo_name (str): repo to fork target_dir (str): cloned repo location upstream_ref (str): name of remote pointing to original repo """ git_hub = Github(auth_github(github_keyname)) print(f'creating fork of repo "{repo_name}"') repo = git_hub.get_repo(f'{org_name}/{repo_name}') fork = repo.create_fork() print(f'cloning repo from "{fork.ssh_url}" into "{target_dir}"') fork_repo = Repo.clone_from(fork.ssh_url, target_dir) fork_repo.create_remote(upstream_ref, repo.ssh_url) def update_from_remote(target_dir, remote_name, remote_branch): """Pull down latest code if repo is clean. Args: target_dir (str): location of repository remote_name (str): name of remote to fetch from remote_branch (str): name of remote branch to fetch from """ repo = Repo(target_dir) if repo.is_dirty(): print(f'skipping update, repo at "{target_dir}" is dirty') elif repo.active_branch.name != remote_branch: print(f'skipping update, not currently on "{remote_branch}" branch') else: print(f'pulling down latest from "{remote_name}/{remote_branch}"') repo.remotes[remote_name].pull(remote_branch) def parse_args(): """Parse and return CLI args.""" parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument( '-u', action='store_true', required=False, dest='update', help='Update code from remote if repo is clean' ) parser.add_argument( '--github-keyname', required=False, default='PyGithub', help='Key containing github auth token' ) parser.add_argument( '--remote-branch', required=False, default='master', help='Branch to merge in from remote on merge update' ) parser.add_argument( '--remote-name', required=False, default='upstream', help='Name of remote ref to use during fork and merge update' ) parser.add_argument( '--org', required=False, default='theorchard', help='GitHub organization to fork repo from' ) parser.add_argument( '--compose-file', required=False, default='docker-compose.yml', help='Location of docker compose file to read' ) parser.add_argument( '--services', nargs='+', required=False, default='*', help='Specific list of service to setup' ) return parser.parse_args() if __name__ == '__main__': main()