diff options
author | Oliver Poignant <oliver@poignant.se> | 2016-05-29 10:39:53 +0200 |
---|---|---|
committer | Oliver Poignant <oliver@poignant.se> | 2016-05-29 10:39:53 +0200 |
commit | b2f95fa7a1f12ea4b8029320db4c7458a9a32feb (patch) | |
tree | 2258bb6d704d19e0a8bd7a043781bfa4f4ae214e /gitautodeploy | |
parent | 110be3c3ea3d34c5cd686061f9dda7e69222e536 (diff) | |
parent | 84e99d89e75b4c2e5e5e8678a15adb9d391daa44 (diff) | |
download | Git-Auto-Deploy-b2f95fa7a1f12ea4b8029320db4c7458a9a32feb.zip Git-Auto-Deploy-b2f95fa7a1f12ea4b8029320db4c7458a9a32feb.tar.gz Git-Auto-Deploy-b2f95fa7a1f12ea4b8029320db4c7458a9a32feb.tar.bz2 |
Merge branch 'master' into development
Diffstat (limited to 'gitautodeploy')
-rw-r--r-- | gitautodeploy/cli/config.py | 330 | ||||
-rw-r--r-- | gitautodeploy/gitautodeploy.py | 501 | ||||
-rw-r--r-- | gitautodeploy/httpserver.py | 204 | ||||
-rw-r--r-- | gitautodeploy/lock.py | 37 | ||||
-rw-r--r-- | gitautodeploy/parsers/bitbucket.py | 4 | ||||
-rw-r--r-- | gitautodeploy/parsers/generic.py | 4 | ||||
-rw-r--r-- | gitautodeploy/parsers/github.py | 30 | ||||
-rw-r--r-- | gitautodeploy/parsers/gitlab.py | 8 | ||||
-rw-r--r-- | gitautodeploy/wrappers/git.py | 58 |
9 files changed, 768 insertions, 408 deletions
diff --git a/gitautodeploy/cli/config.py b/gitautodeploy/cli/config.py index 332b03e..cd8c54c 100644 --- a/gitautodeploy/cli/config.py +++ b/gitautodeploy/cli/config.py @@ -1,4 +1,330 @@ +def get_config_defaults(): + """Get the default configuration values.""" + config = {} + config['quiet'] = False + config['daemon-mode'] = False + config['config'] = None + config['ssh-keygen'] = False + config['force'] = False + config['ssl'] = False + config['ssl-pem-file'] = '~/.gitautodeploy.pem' + config['pidfilepath'] = '~/.gitautodeploy.pid' + config['logfilepath'] = None + config['host'] = '0.0.0.0' + config['port'] = 8001 + config['intercept-stdout'] = True -def generate_config(): - print "Generating config"
\ No newline at end of file + # Record all log levels by default + config['log-level'] = 'NOTSET' + + # Include details with deploy command return codes in HTTP response. Causes + # to await any git pull or deploy command actions before it sends the + # response. + config['detailed-response'] = True + + # Log incoming webhook requests in a way they can be used as test cases + config['log-test-case'] = False + config['log-test-case-dir'] = None + + return config + +def get_config_from_environment(): + """Get configuration values provided as environment variables.""" + import os + + config = {} + + if 'GAD_QUIET' in os.environ: + config['quiet'] = True + + if 'GAD_DAEMON_MODE' in os.environ: + config['daemon-mode'] = True + + if 'GAD_CONFIG' in os.environ: + config['config'] = os.environ['GAD_CONFIG'] + + if 'GAD_SSH_KEYGEN' in os.environ: + config['ssh-keygen'] = True + + if 'GAD_FORCE' in os.environ: + config['force'] = True + + if 'GAD_SSL' in os.environ: + config['ssl'] = True + + if 'GADGAD_SSL_PEM_FILE_SSL' in os.environ: + config['ssl-pem-file'] = os.environ['GAD_SSL_PEM_FILE'] + + if 'GAD_PID_FILE' in os.environ: + config['pidfilepath'] = os.environ['GAD_PID_FILE'] + + if 'GAD_LOG_FILE' in os.environ: + config['logfilepath'] = os.environ['GAD_LOG_FILE'] + + if 'GAD_HOST' in os.environ: + config['host'] = os.environ['GAD_HOST'] + + if 'GAD_PORT' in os.environ: + config['port'] = int(os.environ['GAD_PORT']) + + return config + +def get_config_from_argv(argv): + import argparse + + parser = argparse.ArgumentParser() + + parser.add_argument("-d", "--daemon-mode", + help="run in background (daemon mode)", + dest="daemon-mode", + action="store_true") + + parser.add_argument("-q", "--quiet", + help="supress console output", + dest="quiet", + action="store_true") + + parser.add_argument("-c", "--config", + help="custom configuration file", + dest="config", + type=str) + + parser.add_argument("--ssh-keygen", + help="scan repository hosts for ssh keys", + dest="ssh-keygen", + action="store_true") + + parser.add_argument("--force", + help="kill any process using the configured port", + dest="force", + action="store_true") + + parser.add_argument("--pid-file", + help="specify a custom pid file", + dest="pidfilepath", + type=str) + + parser.add_argument("--log-file", + help="specify a log file", + dest="logfilepath", + type=str) + + parser.add_argument("--host", + help="address to bind to", + dest="host", + type=str) + + parser.add_argument("--port", + help="port to bind to", + dest="port", + type=int) + + parser.add_argument("--ssl", + help="use ssl", + dest="ssl", + action="store_true") + + parser.add_argument("--ssl-pem", + help="path to ssl pem file", + dest="ssl-pem", + type=str) + + config = vars(parser.parse_args(argv)) + + # Delete entries for unprovided arguments + del_keys = [] + for key in config: + if config[key] is None: + del_keys.append(key) + + for key in del_keys: + del config[key] + + return config + +def find_config_file(target_directories=None): + """Attempt to find a path to a config file. Provided paths are scanned + for *.conf(ig)?.json files.""" + import os + import re + import logging + logger = logging.getLogger() + + if not target_directories: + return + + # Remove duplicates + target_directories = list(set(target_directories)) + + # Look for a *conf.json or *config.json + for dir in target_directories: + + if not os.access(dir, os.R_OK): + continue + + for item in os.listdir(dir): + if re.match(r".*conf(ig)?\.json$", item): + path = os.path.realpath(os.path.join(dir, item)) + logger.info("Using '%s' as config" % path) + return path + +def get_config_from_file(path): + """Get configuration values from config file.""" + import logging + import os + logger = logging.getLogger() + + config_file_path = os.path.realpath(path) + logger.info('Using custom configuration file \'%s\'' % config_file_path) + + # Read config data from json file + if config_file_path: + config_data = read_json_file(config_file_path) + else: + logger.info('No configuration file found or specified. Using default values.') + config_data = {} + + return config_data + +def read_json_file(file_path): + import json + import logging + import re + logger = logging.getLogger() + + try: + json_string = open(file_path).read() + + except Exception as e: + logger.critical("Could not load %s file\n" % file_path) + raise e + + try: + # Remove commens from JSON (makes sample config options easier) + regex = r'\s*(#|\/{2}).*$' + regex_inline = r'(:?(?:\s)*([A-Za-z\d\.{}]*)|((?<=\").*\"),?)(?:\s)*(((#|(\/{2})).*)|)$' + lines = json_string.split('\n') + + for index, line in enumerate(lines): + if re.search(regex, line): + if re.search(r'^' + regex, line, re.IGNORECASE): + lines[index] = "" + elif re.search(regex_inline, line): + lines[index] = re.sub(regex_inline, r'\1', line) + + data = json.loads('\n'.join(lines)) + + except Exception as e: + logger.critical("%s file is not valid JSON\n" % file_path) + raise e + + return data + +def init_config(config): + """Initialize config by filling out missing values etc.""" + + import os + import re + import logging + logger = logging.getLogger() + + # Translate any ~ in the path into /home/<user> + if 'pidfilepath' in config and config['pidfilepath']: + config['pidfilepath'] = os.path.expanduser(config['pidfilepath']) + + if 'logfilepath' in config and config['logfilepath']: + config['logfilepath'] = os.path.expanduser(config['logfilepath']) + + if 'repositories' not in config: + config['repositories'] = [] + + for repo_config in config['repositories']: + + # Setup branch if missing + if 'branch' not in repo_config: + repo_config['branch'] = "master" + + # Setup remote if missing + if 'remote' not in repo_config: + repo_config['remote'] = "origin" + + # Setup deploy commands list if not present + if 'deploy_commands' not in repo_config: + repo_config['deploy_commands'] = [] + + # Check if any global pre deploy commands is specified + if 'global_deploy' in config and len(config['global_deploy']) > 0 and len(config['global_deploy'][0]) is not 0: + repo_config['deploy_commands'].insert(0, config['global_deploy'][0]) + + # Check if any repo specific deploy command is specified + if 'deploy' in repo_config: + repo_config['deploy_commands'].append(repo_config['deploy']) + + # Check if any global post deploy command is specified + if 'global_deploy' in config and len(config['global_deploy']) > 1 and len(config['global_deploy'][1]) is not 0: + repo_config['deploy_commands'].append(config['global_deploy'][1]) + + # If a repository is configured with embedded credentials, we create an alternate URL + # without these credentials that cen be used when comparing the URL with URLs referenced + # in incoming web hook requests. + if 'url' in repo_config: + regexp = re.search(r"^(https?://)([^@]+)@(.+)$", repo_config['url']) + if regexp: + repo_config['url_without_usernme'] = regexp.group(1) + regexp.group(3) + + # Translate any ~ in the path into /home/<user> + if 'path' in repo_config: + repo_config['path'] = os.path.expanduser(repo_config['path']) + + if 'filters' not in repo_config: + repo_config['filters'] = [] + + # Rewrite some legacy filter config syntax + for filter in repo_config['filters']: + + # Legacy config syntax? + if ('kind' in filter and filter['kind'] == 'pull-request-handler') or ('type' in filter and filter['type'] == 'pull-request-filter'): + + # Reset legacy values + filter['kind'] = None + filter['type'] = None + + if 'ref' in filter: + filter['pull_request.base.ref'] = filter['ref'] + filter['ref'] = None + + filter['pull_request'] = True + + return config + +def get_repo_config_from_environment(): + """Look for repository config in any defined environment variables. If + found, import to main config.""" + import logging + import os + + if 'GAD_REPO_URL' not in os.environ: + return + + logger = logging.getLogger() + + repo_config = { + 'url': os.environ['GAD_REPO_URL'] + } + + logger.info("Added configuration for '%s' found in environment variables" % os.environ['GAD_REPO_URL']) + + if 'GAD_REPO_BRANCH' in os.environ: + repo_config['branch'] = os.environ['GAD_REPO_BRANCH'] + + if 'GAD_REPO_REMOTE' in os.environ: + repo_config['remote'] = os.environ['GAD_REPO_REMOTE'] + + if 'GAD_REPO_PATH' in os.environ: + repo_config['path'] = os.environ['GAD_REPO_PATH'] + + if 'GAD_REPO_DEPLOY' in os.environ: + repo_config['deploy'] = os.environ['GAD_REPO_DEPLOY'] + + return repo_config
\ No newline at end of file diff --git a/gitautodeploy/gitautodeploy.py b/gitautodeploy/gitautodeploy.py index c4f7ed8..8f3b172 100644 --- a/gitautodeploy/gitautodeploy.py +++ b/gitautodeploy/gitautodeploy.py @@ -15,10 +15,12 @@ class LogInterface(object): class GitAutoDeploy(object): _instance = None _server = None - _config = None + _config = {} + _port = None + _pid = None def __new__(cls, *args, **kwargs): - """Overload constructor to enable Singleton access""" + """Overload constructor to enable singleton access""" if not cls._instance: cls._instance = super(GitAutoDeploy, cls).__new__( cls, *args, **kwargs) @@ -26,21 +28,23 @@ class GitAutoDeploy(object): @staticmethod def debug_diagnosis(port): + """Display information about what process is using the specified port.""" import logging logger = logging.getLogger() pid = GitAutoDeploy.get_pid_on_port(port) if pid is False: - logger.warning('I don\'t know the number of pid that is using my configured port') + logger.warning('Unable to determine what PID is using port %s' % port) return - logger.info('Process with pid number %s is using port %s' % (pid, port)) + logger.info('Process with PID %s is using port %s' % (pid, port)) with open("/proc/%s/cmdline" % pid) as f: cmdline = f.readlines() - logger.info('cmdline ->', cmdline[0].replace('\x00', ' ')) + logger.info('Process with PID %s was started using the command: %s' % (pid, cmdline[0].replace('\x00', ' '))) @staticmethod def get_pid_on_port(port): + """Determine what process (PID) is using a specific port.""" import os with open("/proc/net/tcp", 'r') as f: @@ -78,163 +82,6 @@ class GitAutoDeploy(object): return mpid - def find_config_file_path(self): - """Attempt to find a config file in cwd and script path.""" - - import os - import re - import logging - logger = logging.getLogger() - - # Look for a custom config file if no path is provided as argument - target_directories = [ - os.path.dirname(os.path.realpath(__file__)), # Script path - ] - - # Add current CWD if not identical to script path - if not os.getcwd() in target_directories: - target_directories.append(os.getcwd()) - - target_directories.reverse() - - # Look for a *conf.json or *config.json - for dir in target_directories: - if not os.access(dir, os.R_OK): - continue - for item in os.listdir(dir): - if re.match(r".*conf(ig)?\.json$", item): - path = os.path.realpath(os.path.join(dir, item)) - logger.info("Using '%s' as config" % path) - return path - - return - - def read_json_file(self, file_path): - import json - import logging - import re - logger = logging.getLogger() - - try: - json_string = open(file_path).read() - - except Exception as e: - logger.critical("Could not load %s file\n" % file_path) - raise e - - try: - # Remove commens from JSON (makes sample config options easier) - regex = r'\s*(#|\/{2}).*$' - regex_inline = r'(:?(?:\s)*([A-Za-z\d\.{}]*)|((?<=\").*\"),?)(?:\s)*(((#|(\/{2})).*)|)$' - lines = json_string.split('\n') - - for index, line in enumerate(lines): - if re.search(regex, line): - if re.search(r'^' + regex, line, re.IGNORECASE): - lines[index] = "" - elif re.search(regex_inline, line): - lines[index] = re.sub(regex_inline, r'\1', line) - - data = json.loads('\n'.join(lines)) - - except Exception as e: - logger.critical("%s file is not valid JSON\n" % file_path) - raise e - - return data - - def read_repo_config_from_environment(self, config_data): - """Look for repository config in any defined environment variables. If - found, import to main config.""" - import logging - import os - - if 'GAD_REPO_URL' not in os.environ: - return config_data - - logger = logging.getLogger() - - repo_config = { - 'url': os.environ['GAD_REPO_URL'] - } - - logger.info("Added configuration for '%s' found in environment variables" % os.environ['GAD_REPO_URL']) - - if 'GAD_REPO_BRANCH' in os.environ: - repo_config['branch'] = os.environ['GAD_REPO_BRANCH'] - - if 'GAD_REPO_REMOTE' in os.environ: - repo_config['remote'] = os.environ['GAD_REPO_REMOTE'] - - if 'GAD_REPO_PATH' in os.environ: - repo_config['path'] = os.environ['GAD_REPO_PATH'] - - if 'GAD_REPO_DEPLOY' in os.environ: - repo_config['deploy'] = os.environ['GAD_REPO_DEPLOY'] - - if not 'repositories' in config_data: - config_data['repositories'] = [] - - config_data['repositories'].append(repo_config) - - return config_data - - def init_config(self, config_data): - import os - import re - import logging - logger = logging.getLogger() - - self._config = config_data - - # Translate any ~ in the path into /home/<user> - if 'pidfilepath' in self._config: - self._config['pidfilepath'] = os.path.expanduser(self._config['pidfilepath']) - - if 'repositories' not in self._config: - self._config['repositories'] = [] - - for repo_config in self._config['repositories']: - - # Setup branch if missing - if 'branch' not in repo_config: - repo_config['branch'] = "master" - - # Setup remote if missing - if 'remote' not in repo_config: - repo_config['remote'] = "origin" - - # Setup deploy commands list if not present - if 'deploy_commands' not in repo_config: - repo_config['deploy_commands'] = [] - - # Check if any global pre deploy commands is specified - if 'global_deploy' in self._config and len(self._config['global_deploy'][0]) is not 0: - repo_config['deploy_commands'].insert(0, self._config['global_deploy'][0]) - - # Check if any repo specific deploy command is specified - if 'deploy' in repo_config: - repo_config['deploy_commands'].append(repo_config['deploy']) - - # Check if any global post deploy command is specified - if 'global_deploy' in self._config and len(self._config['global_deploy'][1]) is not 0: - repo_config['deploy_commands'].append(self._config['global_deploy'][1]) - - # If a Bitbucket repository is configured using the https:// URL, a username is usually - # specified in the beginning of the URL. To be able to compare configured Bitbucket - # repositories with incoming web hook events, this username needs to be stripped away in a - # copy of the URL. - if 'url' in repo_config and 'bitbucket_username' not in repo_config: - regexp = re.search(r"^(https?://)([^@]+)@(bitbucket\.org/)(.+)$", repo_config['url']) - if regexp: - repo_config['url_without_usernme'] = regexp.group(1) + regexp.group(3) + regexp.group(4) - - # Translate any ~ in the path into /home/<user> - if 'path' in repo_config: - repo_config['path'] = os.path.expanduser(repo_config['path']) - - return self._config - def clone_all_repos(self): """Iterates over all configured repositories and clones them to their configured paths.""" @@ -244,22 +91,34 @@ class GitAutoDeploy(object): from wrappers import GitWrapper logger = logging.getLogger() + if not 'repositories' in self._config: + return + # Iterate over all configured repositories for repo_config in self._config['repositories']: - + + # Only clone repositories with a configured path + if 'url' not in repo_config: + logger.critical("Repository has no configured URL") + self.close() + self.exit() + return + # Only clone repositories with a configured path if 'path' not in repo_config: - logger.info("Repository %s will not be cloned (no path configured)" % repo_config['url']) + logger.debug("Repository %s will not be cloned (no path configured)" % repo_config['url']) continue - + if os.path.isdir(repo_config['path']) and os.path.isdir(repo_config['path']+'/.git'): - logger.info("Repository %s already present" % repo_config['url']) + logger.debug("Repository %s already present" % repo_config['url']) continue + logger.info("Repository %s not present and needs to be cloned" % repo_config['url']) + # Clone repository - GitWrapper.clone(url=repo_config['url'], branch=repo_config['branch'], path=repo_config['path']) - - if os.path.isdir(repo_config['path']): + ret = GitWrapper.clone(url=repo_config['url'], branch=repo_config['branch'], path=repo_config['path']) + + if ret == 0 and os.path.isdir(repo_config['path']): logger.info("Repository %s successfully cloned" % repo_config['url']) else: logger.error("Unable to clone %s branch of repository %s" % (repo_config['branch'], repo_config['url'])) @@ -289,19 +148,25 @@ class GitAutoDeploy(object): logger.error('Could not find regexp match in path: %s' % url) def kill_conflicting_processes(self): + """Attempt to kill any process already using the configured port.""" import os import logging + import signal logger = logging.getLogger() pid = GitAutoDeploy.get_pid_on_port(self._config['port']) if pid is False: - logger.error('[KILLER MODE] I don\'t know the number of pid ' + - 'that is using my configured port\n[KILLER MODE] ' + - 'Maybe no one? Please, use --force option carefully') + logger.warning('No process is currently using port %s.' % self._config['port']) return False - os.kill(pid, signal.SIGKILL) + if hasattr(signal, 'SIGKILL'): + os.kill(pid, signal.SIGKILL) + elif hasattr(signal, 'SIGHUP'): + os.kill(pid, signal.SIGHUP) + else: + os.kill(pid, 1) + return True def create_pid_file(self): @@ -316,14 +181,27 @@ class GitAutoDeploy(object): def remove_pid_file(self): import os - os.remove(self._config['pidfilepath']) + import errno + if 'pidfilepath' in self._config and self._config['pidfilepath']: + try: + os.remove(self._config['pidfilepath']) + except OSError, e: + if e.errno != errno.ENOENT: # errno.ENOENT = no such file or directory + raise - def exit(self): + def close(self): import sys import logging logger = logging.getLogger() logger.info('Goodbye') self.remove_pid_file() + if 'intercept-stdout' in self._config and self._config['intercept-stdout']: + sys.stdout = self._default_stdout + sys.stderr = self._default_stderr + + def exit(self): + import sys + self.close() sys.exit(0) @staticmethod @@ -358,152 +236,56 @@ class GitAutoDeploy(object): return 0 - def run(self): + def setup(self, config): + """Setup an instance of GAD based on the provided config object.""" import sys from BaseHTTPServer import HTTPServer import socket import os import logging - import argparse from lock import Lock from httpserver import WebhookRequestHandler - # Attempt to retrieve default config values from environment variables - default_quiet_value = 'GAD_QUIET' in os.environ or False - default_daemon_mode_value = 'GAD_DAEMON_MODE' in os.environ or False - default_config_value = 'GAD_CONFIG' in os.environ and os.environ['GAD_CONFIG'] or None - default_ssh_keygen_value = 'GAD_SSH_KEYGEN' in os.environ or False - default_force_value = 'GAD_FORCE' in os.environ or False - default_use_ssl = 'GAD_SSL' in os.environ or False - default_ssl_pem_file_path = 'GAD_SSL_PEM_FILE' in os.environ and os.environ['GAD_SSL_PEM_FILE'] or '~/.gitautodeploy.pem' - default_pid_file_value = 'GAD_PID_FILE' in os.environ and os.environ['GAD_PID_FILE'] or '~/.gitautodeploy.pid' - default_log_file_value = 'GAD_LOG_FILE' in os.environ and os.environ['GAD_LOG_FILE'] or None - default_host_value = 'GAD_HOST' in os.environ and os.environ['GAD_HOST'] or '0.0.0.0' - default_port_value = 'GAD_PORT' in os.environ and int(os.environ['GAD_PORT']) or 8001 - - parser = argparse.ArgumentParser() - - parser.add_argument("-d", "--daemon-mode", - help="run in background (daemon mode)", - default=default_daemon_mode_value, - action="store_true") - - parser.add_argument("-q", "--quiet", - help="supress console output", - default=default_quiet_value, - action="store_true") - - parser.add_argument("-c", "--config", - help="custom configuration file", - default=default_config_value, - type=str) - - parser.add_argument("--ssh-keygen", - help="scan repository hosts for ssh keys", - default=default_ssh_keygen_value, - action="store_true") - - parser.add_argument("--force", - help="kill any process using the configured port", - default=default_force_value, - action="store_true") - - parser.add_argument("--pid-file", - help="specify a custom pid file", - default=default_pid_file_value, - type=str) - - parser.add_argument("--log-file", - help="specify a log file", - default=default_log_file_value, - type=str) - - parser.add_argument("--host", - help="address to bind to", - default=default_host_value, - type=str) - - parser.add_argument("--port", - help="port to bind to", - default=default_port_value, - type=int) - - parser.add_argument("--ssl", - help="use ssl", - default=default_use_ssl, - action="store_true") - - parser.add_argument("--ssl-pem", - help="path to ssl pem file", - default=default_ssl_pem_file_path, - type=str) - - args = parser.parse_args() + # Attatch config values to this instance + self._config = config # Set up logging logger = logging.getLogger() logFormatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") # Enable console output? - if args.quiet: + if ('quiet' in self._config and self._config['quiet']) or ('daemon-mode' in self._config and self._config['daemon-mode']): logger.addHandler(logging.NullHandler()) else: consoleHandler = logging.StreamHandler() consoleHandler.setFormatter(logFormatter) - logger.addHandler(consoleHandler) - - # All logs are recording - logger.setLevel(logging.NOTSET) - - # Look for log file path provided in argument - config_file_path = None - if args.config: - config_file_path = os.path.realpath(args.config) - logger.info('Using custom configuration file \'%s\'' % config_file_path) - - # Try to find a config file on the file system - if not config_file_path: - config_file_path = self.find_config_file_path() - - # Read config data from json file - if config_file_path: - config_data = self.read_json_file(config_file_path) - else: - logger.info('No configuration file found or specified. Using default values.') - config_data = {} - - # Configuration options coming from environment or command line will - # override those coming from config file - if args.pid_file: - config_data['pidfilepath'] = args.pid_file - - if args.log_file: - config_data['logfilepath'] = args.log_file - - if args.host: - config_data['host'] = args.host - if args.port: - config_data['port'] = args.port + # Check if a stream handler is already present (will be if GAD is started by test script) + handler_present = False + for handler in logger.handlers: + if isinstance(handler, type(consoleHandler)): + handler_present = True + break - # Extend config data with any repository defined by environment variables - config_data = self.read_repo_config_from_environment(config_data) + if not handler_present: + logger.addHandler(consoleHandler) - # Initialize config using config file data - self.init_config(config_data) + # Set logging level + if 'log-level' in self._config: + level = logging.getLevelName(self._config['log-level']) + logger.setLevel(level) - # Translate any ~ in the path into /home/<user> - if 'logfilepath' in self._config: - log_file_path = os.path.expanduser(self._config['logfilepath']) - fileHandler = logging.FileHandler(log_file_path) + if 'logfilepath' in self._config and self._config['logfilepath']: + # Translate any ~ in the path into /home/<user> + fileHandler = logging.FileHandler(self._config['logfilepath']) fileHandler.setFormatter(logFormatter) logger.addHandler(fileHandler) - if args.ssh_keygen: + if 'ssh-keygen' in self._config and self._config['ssh-keygen']: logger.info('Scanning repository hosts for ssh keys...') self.ssh_key_scan() - if args.force: + if 'force' in self._config and self._config['force']: logger.info('Attempting to kill any other process currently occupying port %s' % self._config['port']) self.kill_conflicting_processes() @@ -512,15 +294,19 @@ class GitAutoDeploy(object): # Set default stdout and stderr to our logging interface (that writes # to file and console depending on user preference) - sys.stdout = LogInterface(logger.info) - sys.stderr = LogInterface(logger.error) + if 'intercept-stdout' in self._config and self._config['intercept-stdout']: + self._default_stdout = sys.stdout + self._default_stderr = sys.stderr + sys.stdout = LogInterface(logger.info) + sys.stderr = LogInterface(logger.error) - if args.daemon_mode: + if 'daemon-mode' in self._config and self._config['daemon-mode']: logger.info('Starting Git Auto Deploy in daemon mode') GitAutoDeploy.create_daemon() else: logger.info('Git Auto Deploy started') + self._pid = os.getpid() self.create_pid_file() # Clear any existing lock files, with no regard to possible ongoing processes @@ -536,27 +322,72 @@ class GitAutoDeploy(object): self._server = HTTPServer((self._config['host'], self._config['port']), WebhookRequestHandler) - if args.ssl: + + if 'ssl' in self._config and self._config['ssl']: import ssl logger.info("enabling ssl") self._server.socket = ssl.wrap_socket(self._server.socket, - certfile=os.path.expanduser(args.ssl_pem), + certfile=os.path.expanduser(self._config['ssl-pem']), server_side=True) sa = self._server.socket.getsockname() logger.info("Listening on %s port %s", sa[0], sa[1]) + + # Actual port bound to (nessecary when OS picks randomly free port) + self._port = sa[1] + + except socket.error, e: + + logger.critical("Error on socket: %s" % e) + GitAutoDeploy.debug_diagnosis(self._config['port']) + + sys.exit(1) + + def serve_forever(self): + """Start listening for incoming requests.""" + import sys + import socket + import logging + + # Set up logging + logger = logging.getLogger() + + try: self._server.serve_forever() except socket.error, e: + logger.critical("Error on socket: %s" % e) + sys.exit(1) + + except KeyboardInterrupt, e: + logger.info('Requested close by keyboard interrupt signal') + self.stop() + self.exit() + + def handle_request(self): + """Start listening for incoming requests.""" + import sys + import socket + import logging - if not args.daemon_mode: - logger.critical("Error on socket: %s" % e) - GitAutoDeploy.debug_diagnosis(self._config['port']) + # Set up logging + logger = logging.getLogger() + + try: + self._server.handle_request() + except socket.error, e: + logger.critical("Error on socket: %s" % e) sys.exit(1) + + except KeyboardInterrupt, e: + logger.info('Requested close by keyboard interrupt signal') + self.stop() + self.exit() def stop(self): - if self._server is not None: - self._server.socket.close() + if self._server is None: + return + self._server.socket.close() def signal_handler(self, signum, frame): import logging @@ -564,7 +395,8 @@ class GitAutoDeploy(object): self.stop() if signum == 1: - self.run() + self.setup(self._config) + self.serve_forever() return elif signum == 2: @@ -575,16 +407,65 @@ class GitAutoDeploy(object): self.exit() - def main(): import signal from gitautodeploy import GitAutoDeploy + from cli.config import get_config_defaults, get_config_from_environment, get_config_from_argv, find_config_file, get_config_from_file, get_repo_config_from_environment, init_config + import sys + import os app = GitAutoDeploy() - signal.signal(signal.SIGHUP, app.signal_handler) - signal.signal(signal.SIGINT, app.signal_handler) - signal.signal(signal.SIGABRT, app.signal_handler) - signal.signal(signal.SIGPIPE, signal.SIG_IGN) + if hasattr(signal, 'SIGHUP'): + signal.signal(signal.SIGHUP, app.signal_handler) + if hasattr(signal, 'SIGINT'): + signal.signal(signal.SIGINT, app.signal_handler) + if hasattr(signal, 'SIGABRT'): + signal.signal(signal.SIGABRT, app.signal_handler) + if hasattr(signal, 'SIGPIPE') and hasattr(signal, 'SIG_IGN'): + signal.signal(signal.SIGPIPE, signal.SIG_IGN) + + config = get_config_defaults() + + # Get config values from environment variables and commadn line arguments + environment_config = get_config_from_environment() + argv_config = get_config_from_argv(sys.argv[1:]) + + # Merge config values + config.update(environment_config) + config.update(argv_config) + + # Config file path provided? + if 'config' in config and config['config']: + config_file_path = os.path.realpath(config['config']) + + else: + + # Directories to scan for config files + target_directories = [ + os.getcwd(), # cwd + os.path.dirname(os.path.realpath(__file__)) # script path + ] + + config_file_path = find_config_file(target_directories) + + # Config file path provided or found? + if config_file_path: + file_config = get_config_from_file(config_file_path) + config.update(file_config) + + # Extend config data with any repository defined by environment variables + repo_config = get_repo_config_from_environment() + + if repo_config: + + if not 'repositories' in config: + config['repositories'] = [] + + config['repositories'].append(repo_config) + + # Initialize config by expanding with missing values + init_config(config) - app.run()
\ No newline at end of file + app.setup(config) + app.serve_forever() diff --git a/gitautodeploy/httpserver.py b/gitautodeploy/httpserver.py index 99fc574..8032556 100644 --- a/gitautodeploy/httpserver.py +++ b/gitautodeploy/httpserver.py @@ -14,54 +14,102 @@ class WebhookRequestHandler(BaseHTTPRequestHandler): """Invoked on incoming POST requests""" from threading import Timer import logging + import json logger = logging.getLogger() + logger.info('Incoming request from %s:%s' % (self.client_address[0], self.client_address[1])) content_type = self.headers.getheader('content-type') content_length = int(self.headers.getheader('content-length')) request_body = self.rfile.read(content_length) - + + # Test case debug data + test_case = { + 'headers': dict(self.headers), + 'payload': json.loads(request_body), + 'config': {}, + 'expected': {'status': 200, 'data': [{'deploy': 0}]} + } + # Extract request headers and make all keys to lowercase (makes them easier to compare) request_headers = dict(self.headers) request_headers = dict((k.lower(), v) for k, v in request_headers.iteritems()) - ServiceRequestParser = self.figure_out_service_from_request(request_headers, request_body) - - self.send_response(200) - self.send_header('Content-type', 'text/plain') - self.end_headers() - - # Unable to identify the source of the request - if not ServiceRequestParser: - logger.error('Unable to find appropriate handler for request. The source service is not supported.') + try: + + # Will raise a ValueError exception if it fails + ServiceRequestParser = self.figure_out_service_from_request(request_headers, request_body) + + # Unable to identify the source of the request + if not ServiceRequestParser: + self.send_error(400, 'Unrecognized service') + logger.error('Unable to find appropriate handler for request. The source service is not supported.') + test_case['expected']['status'] = 400 + return + + # Send HTTP response before the git pull and/or deploy commands? + if not 'detailed-response' in self._config or not self._config['detailed-response']: + self.send_response(200, 'OK') + self.send_header('Content-type', 'text/plain') + self.end_headers() + + logger.info('Handling the request with %s' % ServiceRequestParser.__name__) + + # Could be GitHubParser, GitLabParser or other + repo_configs, ref, action, webhook_urls = ServiceRequestParser(self._config).get_repo_params_from_request(request_headers, request_body) + logger.debug("Event details - ref: %s; action: %s" % (ref or "master", action)) + + if len(repo_configs) == 0: + self.send_error(400, 'Bad request') + logger.warning('The URLs references in the webhook did not match any repository entry in the config. For this webhook to work, make sure you have at least one repository configured with one of the following URLs; %s' % ', '.join(webhook_urls)) + test_case['expected']['status'] = 400 + return + + # Make git pulls and trigger deploy commands + res = self.process_repositories(repo_configs, ref, action, request_body) + + if 'detailed-response' in self._config and self._config['detailed-response']: + self.send_response(200, 'OK') + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(res)) + self.wfile.close() + + # Add additional test case data + test_case['config'] = { + 'url': 'url' in repo_configs[0] and repo_configs[0]['url'], + 'branch': 'branch' in repo_configs[0] and repo_configs[0]['branch'], + 'remote': 'remote' in repo_configs[0] and repo_configs[0]['remote'], + 'deploy': 'echo test!' + } + + except ValueError, e: + self.send_error(400, 'Unprocessable request') + logger.warning('Unable to process incoming request from %s:%s' % (self.client_address[0], self.client_address[1])) + test_case['expected']['status'] = 400 return - # Could be GitHubParser, GitLabParser or other - repo_configs, ref, action = ServiceRequestParser(self._config).get_repo_params_from_request(request_headers, request_body) + except Exception, e: - logger.info("Event details - ref: %s; action: %s" % (ref or "master", action)) + if 'detailed-response' in self._config and self._config['detailed-response']: + self.send_error(500, 'Unable to process request') - #if success: - # print "Successfullt handled request using %s" % ServiceHandler.__name__ - #else: - # print "Unable to handle request using %s" % ServiceHandler.__name__ + test_case['expected']['status'] = 500 - if len(repo_configs) == 0: - logger.warning('Unable to find any of the repository URLs in the config: %s' % ', '.join(repo_urls)) - return + raise e - # Wait one second before we do git pull (why?) - Timer(1.0, self.process_repositories, (repo_configs, - ref, - action, request_body)).start() + finally: + + # Save the request as a test case + if 'log-test-case' in self._config and self._config['log-test-case']: + self.save_test_case(test_case) def log_message(self, format, *args): """Overloads the default message logging method to allow messages to go through our custom logger instead.""" import logging logger = logging.getLogger() - logger.info("%s - - [%s] %s\n" % (self.client_address[0], - self.log_date_time_string(), + logger.info("%s - %s" % (self.client_address[0], format%args)) def figure_out_service_from_request(self, request_headers, request_body): @@ -74,32 +122,31 @@ class WebhookRequestHandler(BaseHTTPRequestHandler): logger = logging.getLogger() data = json.loads(request_body) + if not isinstance(data, dict): + raise ValueError("Invalid JSON object") + user_agent = 'user-agent' in request_headers and request_headers['user-agent'] content_type = 'content-type' in request_headers and request_headers['content-type'] # Assume GitLab if the X-Gitlab-Event HTTP header is set if 'x-gitlab-event' in request_headers: - logger.info("Received event from GitLab") return parsers.GitLabRequestParser # Assume GitHub if the X-GitHub-Event HTTP header is set elif 'x-github-event' in request_headers: - logger.info("Received event from GitHub") return parsers.GitHubRequestParser # Assume BitBucket if the User-Agent HTTP header is set to # 'Bitbucket-Webhooks/2.0' (or something similar) elif user_agent and user_agent.lower().find('bitbucket') != -1: - logger.info("Received event from BitBucket") return parsers.BitBucketRequestParser # Special Case for Gitlab CI elif content_type == "application/json" and "build_status" in data: - logger.info('Received event from Gitlab CI') return parsers.GitLabCIRequestParser # This handles old GitLab requests and Gogs requests for example. @@ -111,49 +158,80 @@ class WebhookRequestHandler(BaseHTTPRequestHandler): logger.error("Unable to recognize request origin. Don't know how to handle the request.") return - def process_repositories(self, repo_configs, ref, action, request_body): + """Verify that the suggested repositories has matching settings and + issue git pull and/or deploy commands.""" import os import time import logging from wrappers import GitWrapper from lock import Lock import json - + logger = logging.getLogger() data = json.loads(request_body) + result = [] + # Process each matching repository for repo_config in repo_configs: + repo_result = {} + try: # Verify that all filters matches the request (if any filters are specified) if 'filters' in repo_config: - # at least one filter must match + # At least one filter must match for filter in repo_config['filters']: - # all options specified in the filter must match + # All options specified in the filter must match for filter_key, filter_value in filter.iteritems(): - # support for earlier version so it's non-breaking functionality + # Ignore filters with value None (let them pass) + if filter_value == None: + continue + + # Support for earlier version so it's non-breaking functionality if filter_key == 'action' and filter_value == action: continue - if filter_key not in data or filter_value != data[filter_key]: - raise FilterMatchError() + # Interpret dots in filter name as path notations + node_value = data + for node_key in filter_key.split('.'): + + # If the path is not valid the filter does not match + if not node_key in node_value: + logger.info("Filter '%s'' does not match since the path is invalid" % (filter_key)) + raise FilterMatchError() + + node_value = node_value[node_key] + + if filter_value == node_value: + continue + + # If the filter value is set to True. the filter + # will pass regardless of the actual value + if filter_value == True: + continue + + logger.info("Filter '%s'' does not match ('%s' != '%s')" % (filter_key, filter_value, (str(node_value)[:75] + '..') if len(str(node_value)) > 75 else str(node_value))) + + raise FilterMatchError() except FilterMatchError as e: # Filter does not match, do not process this repo config continue - + # In case there is no path configured for the repository, no pull will # be made. if not 'path' in repo_config: - GitWrapper.deploy(repo_config) + res = GitWrapper.deploy(repo_config) + repo_result['deploy'] = res + result.append(repo_result) continue - + running_lock = Lock(os.path.join(repo_config['path'], 'status_running')) waiting_lock = Lock(os.path.join(repo_config['path'], 'status_waiting')) try: @@ -173,15 +251,27 @@ class WebhookRequestHandler(BaseHTTPRequestHandler): time.sleep(5) n = 4 - while 0 < n and 0 != GitWrapper.pull(repo_config): + res = None + while n > 0: + + # Attempt to pull up a maximum of 4 times + res = GitWrapper.pull(repo_config) + repo_result['git pull'] = res + + # Return code indicating success? + if res == 0: + break + n -= 1 if 0 < n: - GitWrapper.deploy(repo_config) + res = GitWrapper.deploy(repo_config) + repo_result['deploy'] = res - except Exception as e: - logger.error('Error during \'pull\' or \'deploy\' operation on path: %s' % repo_config['path']) - logger.error(e) + #except Exception as e: + # logger.error('Error during \'pull\' or \'deploy\' operation on path: %s' % repo_config['path']) + # logger.error(e) + # raise e finally: @@ -192,3 +282,27 @@ class WebhookRequestHandler(BaseHTTPRequestHandler): # Release the lock if it's ours if waiting_lock.has_lock(): waiting_lock.release() + + result.append(repo_result) + + return result + + def save_test_case(self, test_case): + """Log request information in a way it can be used as a test case.""" + import time + import json + import os + + # Mask some header values + masked_headers = ['x-github-delivery', 'x-hub-signature'] + for key in test_case['headers']: + if key in masked_headers: + test_case['headers'][key] = 'xxx' + + target = '%s-%s.tc.json' % (self.client_address[0], time.strftime("%Y%m%d%H%M%S")) + if 'log-test-case-dir' in self._config and self._config['log-test-case-dir']: + target = os.path.join(self._config['log-test-case-dir'], target) + + file = open(target, 'w') + file.write(json.dumps(test_case, sort_keys=True, indent=4)) + file.close()
\ No newline at end of file diff --git a/gitautodeploy/lock.py b/gitautodeploy/lock.py index 8bd8b32..f894bb3 100644 --- a/gitautodeploy/lock.py +++ b/gitautodeploy/lock.py @@ -1,12 +1,15 @@ +from lockfile import LockFile + class Lock(): """Simple implementation of a mutex lock using the file systems. Works on *nix systems.""" path = None - _has_lock = False + lock = None def __init__(self, path): self.path = path + self.lock = LockFile(path) def obtain(self): import os @@ -14,39 +17,31 @@ class Lock(): logger = logging.getLogger() try: - os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) - self._has_lock = True - logger.info("Successfully obtained lock: %s" % self.path) - except OSError: + self.lock.acquire(0) + logger.debug("Successfully obtained lock: %s" % self.path) + except AlreadyLocked: return False - else: - return True + + return True def release(self): import os import logging logger = logging.getLogger() - if not self._has_lock: + if not self.has_lock(): raise Exception("Unable to release lock that is owned by another process") - try: - os.remove(self.path) - logger.info("Successfully released lock: %s" % self.path) - finally: - self._has_lock = False + + self.lock.release() + logger.debug("Successfully released lock: %s" % self.path) def has_lock(self): - return self._has_lock + return self.lock.i_am_locking() def clear(self): import os import logging logger = logging.getLogger() - try: - os.remove(self.path) - except OSError: - pass - finally: - logger.info("Successfully cleared lock: %s" % self.path) - self._has_lock = False
\ No newline at end of file + self.lock.break_lock() + logger.debug("Successfully cleared lock: %s" % self.path) diff --git a/gitautodeploy/parsers/bitbucket.py b/gitautodeploy/parsers/bitbucket.py index 791b491..353435c 100644 --- a/gitautodeploy/parsers/bitbucket.py +++ b/gitautodeploy/parsers/bitbucket.py @@ -13,7 +13,7 @@ class BitBucketRequestParser(WebhookRequestParser): ref = "" action = "" - logger.info("Received event from BitBucket") + logger.debug("Received event from BitBucket") if 'repository' not in data: logger.error("Unable to recognize data format") @@ -34,4 +34,4 @@ class BitBucketRequestParser(WebhookRequestParser): # Get a list of configured repositories that matches the incoming web hook reqeust repo_configs = self.get_matching_repo_configs(repo_urls) - return repo_configs, ref or "master", action
\ No newline at end of file + return repo_configs, ref or "master", action, repo_urls
\ No newline at end of file diff --git a/gitautodeploy/parsers/generic.py b/gitautodeploy/parsers/generic.py index 7c7c7f5..3247662 100644 --- a/gitautodeploy/parsers/generic.py +++ b/gitautodeploy/parsers/generic.py @@ -13,7 +13,7 @@ class GenericRequestParser(WebhookRequestParser): ref = "" action = "" - logger.info("Received event from unknown origin. Assume generic data format.") + logger.debug("Received event from unknown origin. Assume generic data format.") if 'repository' not in data: logger.error("Unable to recognize data format") @@ -27,4 +27,4 @@ class GenericRequestParser(WebhookRequestParser): # Get a list of configured repositories that matches the incoming web hook reqeust repo_configs = self.get_matching_repo_configs(repo_urls) - return repo_configs, ref or "master", action
\ No newline at end of file + return repo_configs, ref or "master", action, repo_urls
\ No newline at end of file diff --git a/gitautodeploy/parsers/github.py b/gitautodeploy/parsers/github.py index 028663d..7077def 100644 --- a/gitautodeploy/parsers/github.py +++ b/gitautodeploy/parsers/github.py @@ -15,7 +15,7 @@ class GitHubRequestParser(WebhookRequestParser): github_event = 'x-github-event' in request_headers and request_headers['x-github-event'] - logger.info("Received '%s' event from GitHub" % github_event) + logger.debug("Received '%s' event from GitHub" % github_event) if 'repository' not in data: logger.error("Unable to recognize data format") @@ -30,16 +30,34 @@ class GitHubRequestParser(WebhookRequestParser): if 'base' in data['pull_request']: if 'ref' in data['pull_request']['base']: ref = data['pull_request']['base']['ref'] - logger.info("Pull request to branch '%s' was fired" % ref) + logger.debug("Pull request to branch '%s' was fired" % ref) elif 'ref' in data: ref = data['ref'] - logger.info("Push to branch '%s' was fired" % ref) + logger.debug("Push to branch '%s' was fired" % ref) if 'action' in data: action = data['action'] - logger.info("Action '%s' was fired" % action) + logger.debug("Action '%s' was fired" % action) # Get a list of configured repositories that matches the incoming web hook reqeust - repo_configs = self.get_matching_repo_configs(repo_urls) + items = self.get_matching_repo_configs(repo_urls) - return repo_configs, ref or "master", action
\ No newline at end of file + repo_configs = [] + for repo_config in items: + + # Validate secret token if present + if 'secret-token' in repo_config and 'x-hub-signature' in request_headers: + if not self.verify_signature(repo_config['secret-token'], request_body, request_headers['x-hub-signature']): + logger.warning("Request signature does not match the 'secret-token' configured for repository %s." % repo_config['url']) + continue + + repo_configs.append(repo_config) + + return repo_configs, ref or "master", action, repo_urls + + def verify_signature(self, token, body, signature): + import hashlib + import hmac + + result = "sha1=" + hmac.new(str(token), body, hashlib.sha1).hexdigest() + return result == signature diff --git a/gitautodeploy/parsers/gitlab.py b/gitautodeploy/parsers/gitlab.py index 580fed2..3551dc6 100644 --- a/gitautodeploy/parsers/gitlab.py +++ b/gitautodeploy/parsers/gitlab.py @@ -15,7 +15,7 @@ class GitLabRequestParser(WebhookRequestParser): gitlab_event = 'x-gitlab-event' in request_headers and request_headers['x-gitlab-event'] - logger.info("Received '%s' event from GitLab" % gitlab_event) + logger.debug("Received '%s' event from GitLab" % gitlab_event) if 'repository' not in data: logger.error("Unable to recognize data format") @@ -37,7 +37,7 @@ class GitLabRequestParser(WebhookRequestParser): # Get a list of configured repositories that matches the incoming web hook reqeust repo_configs = self.get_matching_repo_configs(repo_urls) - return repo_configs, ref or "master", action + return repo_configs, ref or "master", action, repo_urls class GitLabCIRequestParser(WebhookRequestParser): @@ -53,7 +53,7 @@ class GitLabCIRequestParser(WebhookRequestParser): ref = "" action = "" - logger.info('Received event from Gitlab CI') + logger.debug('Received event from Gitlab CI') if 'push_data' not in data: logger.error("Unable to recognize data format") @@ -71,4 +71,4 @@ class GitLabCIRequestParser(WebhookRequestParser): # Get a list of configured repositories that matches the incoming web hook reqeust repo_configs = self.get_matching_repo_configs(repo_urls) - return repo_configs, ref or "master", action
\ No newline at end of file + return repo_configs, ref or "master", action, repo_urls
\ No newline at end of file diff --git a/gitautodeploy/wrappers/git.py b/gitautodeploy/wrappers/git.py index 98a7361..3f29341 100644 --- a/gitautodeploy/wrappers/git.py +++ b/gitautodeploy/wrappers/git.py @@ -10,33 +10,50 @@ class GitWrapper(): """Pulls the latest version of the repo from the git server""" import logging from process import ProcessWrapper - + import os + import platform + logger = logging.getLogger() - logger.info("Post push request received") + logger.info("Updating repository %s" % repo_config['path']) # Only pull if there is actually a local copy of the repository if 'path' not in repo_config: logger.info('No local repository path configured, no pull will occure') return 0 - - logger.info('Updating ' + repo_config['path']) - cmd = 'unset GIT_DIR ' + \ - '&& git fetch ' + repo_config['remote'] + \ - '&& git reset --hard ' + repo_config['remote'] + '/' + repo_config['branch'] + ' ' + \ - '&& git submodule init ' + \ - '&& git submodule update' + commands = [] + + if platform.system().lower() == "windows": + # This assumes Git for Windows is installed. + commands.append('"\Program Files\Git\usr\\bin\\bash.exe" -c "cd ' + repo_config['path']) + + commands.append('unset GIT_DIR') + commands.append('git fetch ' + repo_config['remote']) + commands.append('git reset --hard ' + repo_config['remote'] + '/' + repo_config['branch']) + commands.append('git submodule init') + commands.append('git submodule update') + #commands.append('git update-index --refresh') + + # All commands needs to success + for command in commands: + res = ProcessWrapper().call(command, cwd=repo_config['path'], shell=True) + + if res != 0: + logger.error("Command '%s' failed with exit code %s" % (command, res)) + break - # '&& git update-index --refresh ' +\ - res = ProcessWrapper().call([cmd], cwd=repo_config['path'], shell=True) - logger.info('Pull result: ' + str(res)) + if res == 0 and os.path.isdir(repo_config['path']): + logger.info("Repository %s successfully updated" % repo_config['path']) + else: + logger.error("Unable to update repository %s" % repo_config['path']) return int(res) @staticmethod def clone(url, branch, path): from process import ProcessWrapper - ProcessWrapper().call(['git clone --recursive ' + url + ' -b ' + branch + ' ' + path], shell=True) + res = ProcessWrapper().call(['git clone --recursive ' + url + ' -b ' + branch + ' ' + path], shell=True) + return int(res) @staticmethod def deploy(repo_config): @@ -48,10 +65,19 @@ class GitWrapper(): if 'path' in repo_config: path = repo_config['path'] - logger.info('Executing deploy command(s)') - + if not 'deploy_commands' in repo_config or len(repo_config['deploy_commands']) == 0: + logger.info('No deploy commands configured') + return [] + + logger.info('Executing %s deploy commands' % str(len(repo_config['deploy_commands']))) + # Use repository path as default cwd when executing deploy commands cwd = (repo_config['path'] if 'path' in repo_config else None) + res = [] for cmd in repo_config['deploy_commands']: - ProcessWrapper().call([cmd], cwd=cwd, shell=True)
\ No newline at end of file + res.append(ProcessWrapper().call([cmd], cwd=cwd, shell=True)) + + logger.info('%s commands executed with status; %s' % (str(len(res)), str(res))) + + return res |