Source code for webhook2lambda2sqs.terraform_runner

"""
The latest version of this package is available at:
<http://github.com/jantman/webhook2lambda2sqs>

################################################################################
Copyright 2016 Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>

    This file is part of webhook2lambda2sqs, also known as webhook2lambda2sqs.

    webhook2lambda2sqs is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    webhook2lambda2sqs is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with webhook2lambda2sqs.  If not, see <http://www.gnu.org/licenses/>.

The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/webhook2lambda2sqs> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################

AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
"""

import logging
from webhook2lambda2sqs.utils import run_cmd
import re
import json

logger = logging.getLogger(__name__)

TF_CONFIG_NAME = 'webhook2lambda2sqs.tf.json'


[docs]class TerraformRunner(object): def __init__(self, config, tf_path): """ Initialize the Terraform command runner. :param config: program configuration :type config: :py:class:`~.Config` :param tf_path: path to terraform binary :type tf_path: str """ self.config = config self.tf_path = tf_path # if we fail getting the version, assume newest self.tf_version = (999, 999, 999) self._validate()
[docs] def _validate(self): """ Confirm that we can run terraform (by calling its version action) and then validate the configuration. """ try: out = self._run_tf('version') except: raise Exception('ERROR: executing \'%s version\' failed; is ' 'terraform installed and is the path to it (%s) ' 'correct?' % (self.tf_path, self.tf_path)) res = re.search(r'Terraform v(\d+)\.(\d+)\.(\d+)', out) if res is None: logger.error('Unable to determine terraform version; will not ' 'validate config. Note that this may cause problems ' 'when using older Terraform versions. This program ' 'requires Terraform >= 0.6.16.') return self.tf_version = ( int(res.group(1)), int(res.group(2)), int(res.group(3)) ) logger.debug('Terraform version: %s', self.tf_version) if self.tf_version < (0, 6, 16): raise Exception('This program requires Terraform >= 0.6.16, as ' 'that version introduces a bug fix for working ' 'with api_gateway_integration_response resources; ' 'see: https://github.com/hashicorp/terraform/pull' '/5893') try: self._run_tf('validate', ['.']) except Exception as ex: logger.critical("Terraform config validation failed. " "This is almost certainly a bug in " "webhook2lambda2sqs; please re-run with '-vv' and " "open a bug at <https://github.com/jantman/" "webhook2lambda2sqs/issues>. Exception: %s", ex) raise Exception( 'ERROR: Terraform config validation failed: %s' % ex )
[docs] def _args_for_remote(self): """ Generate arguments for 'terraform remote config'. Return None if not present in configuration. :return: list of args for 'terraform remote config' or None :rtype: :std:term:`list` """ conf = self.config.get('terraform_remote_state') if conf is None: return None args = ['-backend=%s' % conf['backend']] for k, v in sorted(conf['config'].items()): args.append('-backend-config="%s=%s"' % (k, v)) return args
[docs] def _set_remote(self, stream=False): """ Call :py:meth:`~._args_for_remote`; if the return value is not None, execute 'terraform remote config' with those arguments and ensure it exits 0. :param stream: whether or not to stream TF output in realtime :type stream: bool """ args = self._args_for_remote() if args is None: logger.debug('_args_for_remote() returned None; not configuring ' 'terraform remote') return logger.warning('Setting terraform remote config: %s', ' '.join(args)) args = ['config'] + args self._run_tf('remote', cmd_args=args, stream=stream) logger.info('Terraform remote configured.')
[docs] def _run_tf(self, cmd, cmd_args=[], stream=False): """ Run a single terraform command via :py:func:`~.utils.run_cmd`; raise exception on non-zero exit status. :param cmd: terraform command to run :type cmd: str :param cmd_args: arguments to command :type cmd_args: :std:term:`list` :return: command output :rtype: str :raises: Exception on non-zero exit """ args = [self.tf_path, cmd] + cmd_args arg_str = ' '.join(args) logger.info('Running terraform command: %s', arg_str) out, retcode = run_cmd(arg_str, stream=stream) if retcode != 0: logger.critical('Terraform command (%s) failed with exit code ' '%d:\n%s', arg_str, retcode, out) raise Exception('terraform %s failed' % cmd) return out
[docs] def plan(self, stream=False): """ Run a 'terraform plan' :param stream: whether or not to stream TF output in realtime :type stream: bool """ self._setup_tf(stream=stream) args = ['-input=false', '-refresh=true', '.'] logger.warning('Running terraform plan: %s', ' '.join(args)) out = self._run_tf('plan', cmd_args=args, stream=stream) if stream: logger.warning('Terraform plan finished successfully.') else: logger.warning("Terraform plan finished successfully:\n%s", out)
[docs] def _taint_deployment(self, stream=False): """ Run 'terraform taint aws_api_gateway_deployment.depl' to taint the deployment resource. This is a workaround for https://github.com/hashicorp/terraform/issues/6613 :param stream: whether or not to stream TF output in realtime :type stream: bool """ args = ['aws_api_gateway_deployment.depl'] logger.warning('Running terraform taint: %s as workaround for ' '<https://github.com/hashicorp/terraform/issues/6613>', ' '.join(args)) out = self._run_tf('taint', cmd_args=args, stream=stream) if stream: logger.warning('Terraform taint finished successfully.') else: logger.warning("Terraform taint finished successfully:\n%s", out)
[docs] def apply(self, stream=False): """ Run a 'terraform apply' :param stream: whether or not to stream TF output in realtime :type stream: bool """ self._setup_tf(stream=stream) try: self._taint_deployment(stream=stream) except Exception: pass args = ['-input=false', '-refresh=true', '.'] logger.warning('Running terraform apply: %s', ' '.join(args)) out = self._run_tf('apply', cmd_args=args, stream=stream) if stream: logger.warning('Terraform apply finished successfully.') else: logger.warning("Terraform apply finished successfully:\n%s", out) self._show_outputs()
[docs] def _show_outputs(self): """ Print the terraform outputs. """ outs = self._get_outputs() print("\n\n" + '=> Terraform Outputs:') for k in sorted(outs): print('%s = %s' % (k, outs[k]))
[docs] def _get_outputs(self): """ Return a dict of the terraform outputs. :return: dict of terraform outputs :rtype: dict """ if self.tf_version >= (0, 7, 0): logger.debug('Running: terraform output') res = self._run_tf('output', cmd_args=['-json']) outs = json.loads(res.strip()) res = {} for k in outs.keys(): if isinstance(outs[k], type({})): res[k] = outs[k]['value'] else: res[k] = outs[k] logger.debug('Terraform outputs: %s', res) return res logger.debug('Running: terraform output') res = self._run_tf('output') outs = {} for line in res.split("\n"): line = line.strip() if line == '': continue parts = line.split(' = ', 1) outs[parts[0]] = parts[1] logger.debug('Terraform outputs: %s', outs) return outs
[docs] def destroy(self, stream=False): """ Run a 'terraform destroy' :param stream: whether or not to stream TF output in realtime :type stream: bool """ self._setup_tf(stream=stream) args = ['-refresh=true', '-force', '.'] logger.warning('Running terraform destroy: %s', ' '.join(args)) out = self._run_tf('destroy', cmd_args=args, stream=stream) if stream: logger.warning('Terraform destroy finished successfully.') else: logger.warning("Terraform destroy finished successfully:\n%s", out)
[docs] def _setup_tf(self, stream=False): """ Setup terraform; either 'remote config' or 'init' depending on version. """ if self.tf_version < (0, 9, 0): self._set_remote(stream=stream) return self._run_tf('init', stream=stream) logger.info('Terraform initialized')