Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI support for extra user supplied terraform files #1267

Merged
merged 15 commits into from
Jul 2, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"region": "us-east-1"
},
"general": {
"terraform_files": [],
"matcher_locations": [
"matchers"
],
Expand Down
4 changes: 4 additions & 0 deletions docs/source/config-global.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ Configuration

{
"general": {
"terraform_files": [
"/absolute/path/to/extra/terraform/file.tf"
],
"matcher_locations": [
"matchers"
],
Expand Down Expand Up @@ -90,6 +93,7 @@ Options
``scheduled_query_locations`` Yes ``["scheduled_queries"]`` List of local paths where ``scheduled_queries`` are defined
``publisher_locations`` Yes ``["publishers"]`` List of local paths where ``publishers`` are defined
``third_party_libraries`` No ``["pathlib2==2.3.5"]`` List of third party dependencies that should be installed via ``pip`` at deployment time. These are libraries needed in rules, custom code, etc that are defined in one of the above settings.
``terraform_files`` No ``[]`` List of local paths to Terraform files that should be included as part of this StreamAlert deployment
============================= ============= ========================= ===============


Expand Down
21 changes: 19 additions & 2 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,17 @@

terraform <cmd>
"""
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
import sys

from streamalert import __version__ as version
from streamalert_cli.config import DEFAULT_CONFIG_PATH
from streamalert_cli.runner import cli_runner, StreamAlertCLICommandRepository
from streamalert_cli.utils import DirectoryType, generate_subparser
from streamalert_cli.utils import (
DirectoryType,
generate_subparser,
UniqueSortedFileListAppendAction,
)


def build_parser():
Expand Down Expand Up @@ -79,6 +83,19 @@ def build_parser():
type=DirectoryType()
)

parser.add_argument(
'-t',
'--terraform-file',
dest='terraform_files',
help=(
'Path to one or more additional Terraform configuration '
'files to include in this deployment'
),
action=UniqueSortedFileListAppendAction,
type=FileType('r'),
default=[]
)

# Dynamically generate subparsers, and create a 'commands' block for the prog description
command_block = []
subparsers = parser.add_subparsers(dest='command', required=True)
Expand Down
19 changes: 18 additions & 1 deletion streamalert_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import os
import re
import string
import tempfile

from streamalert.apps import StreamAlertApp
from streamalert.shared import CLUSTERED_FUNCTIONS, config, metrics
Expand All @@ -32,9 +33,18 @@
class CLIConfig:
"""A class to load, modify, and display the StreamAlertCLI Config"""

def __init__(self, config_path):
def __init__(self, config_path, extra_terraform_files=None):
self.config_path = config_path
self.config = config.load_config(config_path)
self._terraform_files = extra_terraform_files or []
temp_dir = tempfile.TemporaryDirectory(prefix='streamalert_terraform-')
self.terraform_temp_path = temp_dir.name
# Store the name, but remove the directory since shutil.copytree will complain
# about a directory existing
# TODO: Python 3.8 has added the 'dir_exists_ok' option to shutil.copytree,
# so this can be removed when we update to 3.8
# See: https://docs.python.org/3.8/library/shutil.html#shutil.copytree
temp_dir.cleanup()

def __repr__(self):
return str(self.config)
Expand All @@ -58,6 +68,13 @@ def clusters(self):
"""Return list of cluster configuration keys"""
return list(self.config['clusters'].keys())

@property
def terraform_files(self):
"""Return set of terraform files to include with this deployment"""
return set(self._terraform_files).union(
self.config['global']['general'].get('terraform_files', [])
)

def set_prefix(self, prefix):
"""Set the Org Prefix in Global settings"""
if not isinstance(prefix, str):
Expand Down
16 changes: 9 additions & 7 deletions streamalert_cli/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@

from streamalert.shared.logger import get_logger

from streamalert_cli.terraform import TERRAFORM_FILES_PATH


LOGGER = get_logger(__name__)

Expand All @@ -39,7 +37,7 @@
}


def run_command(runner_args, **kwargs):
def run_command(runner_args, cwd='./', **kwargs):
"""Helper function to run commands with error handling.

Args:
Expand All @@ -52,7 +50,6 @@ def run_command(runner_args, **kwargs):
"""
default_error_message = "An error occurred while running: {}".format(' '.join(runner_args))
error_message = kwargs.get('error_message', default_error_message)
cwd = kwargs.get('cwd', TERRAFORM_FILES_PATH)

# Add the -force-copy flag for s3 state copying to suppress dialogs that
# the user must type 'yes' into.
Expand Down Expand Up @@ -98,12 +95,13 @@ def continue_prompt(message=None):
return response == 'yes'


def tf_runner(action='apply', refresh=True, auto_approve=False, targets=None):
def tf_runner(config, action='apply', refresh=True, auto_approve=False, targets=None):
"""Terraform wrapper to build StreamAlert infrastructure.

Resolves modules with `terraform get` before continuing.

Args:
config (CLIConfig): Loaded StreamAlert config
action (str): Terraform action ('apply' or 'destroy').
refresh (bool): If True, Terraform will refresh its state before applying the change.
auto_approve (bool): If True, Terraform will *not* prompt the user for approval.
Expand All @@ -113,8 +111,12 @@ def tf_runner(action='apply', refresh=True, auto_approve=False, targets=None):
Returns:
bool: True if the terraform command was successful
"""
LOGGER.info('Initializing StreamAlert')
if not run_command(['terraform', 'init'], cwd=config.terraform_temp_path):
return False

LOGGER.debug('Resolving Terraform modules')
if not run_command(['terraform', 'get'], quiet=True):
if not run_command(['terraform', 'get'], cwd=config.terraform_temp_path, quiet=True):
return False

tf_command = ['terraform', action, '-refresh={}'.format(str(refresh).lower())]
Expand All @@ -130,7 +132,7 @@ def tf_runner(action='apply', refresh=True, auto_approve=False, targets=None):
if targets:
tf_command.extend('-target={}'.format(x) for x in targets)

return run_command(tf_command)
return run_command(tf_command, cwd=config.terraform_temp_path)


def check_credentials():
Expand Down
1 change: 1 addition & 0 deletions streamalert_cli/kinesis/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ def handler(cls, options, config):
return False

return tf_runner(
config,
action='apply',
targets=[
'module.{}_{}'.format('kinesis_events', cluster) for cluster in config.clusters()
Expand Down
2 changes: 1 addition & 1 deletion streamalert_cli/manage_lambda/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def deploy(config, functions, clusters=None):
LOGGER.debug('Applying terraform targets: %s', ', '.join(sorted(deploy_targets)))

# Terraform applies the new package and publishes a new version
return helpers.tf_runner(targets=deploy_targets)
return helpers.tf_runner(config, targets=deploy_targets)


def _update_rule_table(options, config):
Expand Down
5 changes: 2 additions & 3 deletions streamalert_cli/manage_lambda/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

from streamalert.shared.logger import get_logger
from streamalert_cli.helpers import run_command
from streamalert_cli.terraform import TERRAFORM_FILES_PATH

LOGGER = get_logger(__name__)

Expand All @@ -29,7 +28,7 @@ class LambdaPackage:
# The name of the directory to package and basename of the generated .zip file
PACKAGE_NAME = 'streamalert'

# The configurable items for user specified files
# The configurable items for user specified files to include in deployment pacakge
CONFIG_EXTRAS = {
'matcher_locations',
'rule_locations',
Expand Down Expand Up @@ -86,7 +85,7 @@ def create(self):
# Zip it all up
# Build these in the top-level of the terraform directory as streamalert.zip
result = shutil.make_archive(
os.path.join(TERRAFORM_FILES_PATH, self.PACKAGE_NAME),
os.path.join(self.config.terraform_temp_path, self.PACKAGE_NAME),
'zip',
self.temp_package_path
)
Expand Down
4 changes: 1 addition & 3 deletions streamalert_cli/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
from streamalert_cli.terraform.generate import TerraformGenerateCommand
from streamalert_cli.terraform.handlers import (
TerraformBuildCommand,
TerraformCleanCommand,
TerraformDestroyCommand,
TerraformInitCommand,
TerraformListTargetsCommand,
Expand All @@ -59,7 +58,7 @@ def cli_runner(args):
Returns:
bool: False if errors occurred, True otherwise
"""
config = CLIConfig(args.config_dir)
config = CLIConfig(args.config_dir, args.terraform_files)

set_logger_levels(args.debug)

Expand Down Expand Up @@ -93,7 +92,6 @@ def register_all(cls):
'app': AppCommand,
'athena': AthenaCommand,
'build': TerraformBuildCommand,
'clean': TerraformCleanCommand,
'configure': ConfigureCommand,
'create-alarm': CreateMetricAlarmCommand,
'create-cluster-alarm': CreateClusterMetricAlarmCommand,
Expand Down
48 changes: 31 additions & 17 deletions streamalert_cli/terraform/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
"""
from fnmatch import fnmatch
import json
import os
import shutil

from streamalert.shared.config import ConfigError, firehose_alerts_bucket
from streamalert.shared.logger import get_logger
Expand Down Expand Up @@ -373,14 +373,6 @@ def generate_cluster(config, cluster_name):
return cluster_dict


def cleanup_old_tf_files():
"""
Cleanup old *.tf.json files
"""
for terraform_file in os.listdir(TERRAFORM_FILES_PATH):
if fnmatch(terraform_file, '*.tf.json'):
os.remove(os.path.join(TERRAFORM_FILES_PATH, terraform_file))


class TerraformGenerateCommand(CLICommand):
description = 'Generate Terraform files from JSON cluster files'
Expand All @@ -394,6 +386,28 @@ def handler(cls, options, config):
return terraform_generate_handler(config, check_creds=False)


def _copy_terraform_files(config):
"""Copy all packaged terraform files and terraform files provided by the user to temp

Args:
config (CLIConfig): Loaded StreamAlert config
"""
# Copy the packaged terraform files to temp
# Currently this ignores *.tf.json and *.zip files, in the instance that these
# exist in current deployments. This can be removed in a future release.
shutil.copytree(
TERRAFORM_FILES_PATH,
config.terraform_temp_path,
ignore=shutil.ignore_patterns('*.tf.json', '*.zip') # TODO: remove this eventually
)

# Copy any additional user provided terraform files to temp
for item in config.terraform_files:
shutil.copy2(item, config.terraform_temp_path)

LOGGER.info('Copied Terraform configuration to \'%s\'', config.terraform_temp_path)


def terraform_generate_handler(config, init=False, check_tf=True, check_creds=True):
"""Generate all Terraform plans for the configured clusters.

Expand All @@ -412,13 +426,13 @@ def terraform_generate_handler(config, init=False, check_tf=True, check_creds=Tr
if check_tf and not terraform_check():
return False

cleanup_old_tf_files()
_copy_terraform_files(config)

# Setup the main.tf.json file
LOGGER.debug('Generating cluster file: main.tf.json')
_create_terraform_module_file(
generate_main(config, init=init),
os.path.join(TERRAFORM_FILES_PATH, 'main.tf.json')
os.path.join(config.terraform_temp_path, 'main.tf.json')
)

# Setup Artifact Extractor if it is enabled.
Expand Down Expand Up @@ -451,21 +465,21 @@ def terraform_generate_handler(config, init=False, check_tf=True, check_creds=Tr
file_name = '{}.tf.json'.format(cluster)
_create_terraform_module_file(
cluster_dict,
os.path.join(TERRAFORM_FILES_PATH, file_name),
os.path.join(config.terraform_temp_path, file_name),
)

metric_filters = generate_aggregate_cloudwatch_metric_filters(config)
if metric_filters:
_create_terraform_module_file(
metric_filters,
os.path.join(TERRAFORM_FILES_PATH, 'metric_filters.tf.json')
os.path.join(config.terraform_temp_path, 'metric_filters.tf.json')
)

metric_alarms = generate_aggregate_cloudwatch_metric_alarms(config)
if metric_alarms:
_create_terraform_module_file(
metric_alarms,
os.path.join(TERRAFORM_FILES_PATH, 'metric_alarms.tf.json')
os.path.join(config.terraform_temp_path, 'metric_alarms.tf.json')
)

# Setup Threat Intel Downloader Lambda function if it is enabled
Expand Down Expand Up @@ -531,7 +545,7 @@ def _generate_lookup_tables_settings(config):
"""
Generates .tf.json file for LookupTables
"""
tf_file_name = os.path.join(TERRAFORM_FILES_PATH, 'lookup_tables.tf.json')
tf_file_name = os.path.join(config.terraform_temp_path, 'lookup_tables.tf.json')

if not config['lookup_tables'].get('enabled', False):
remove_temp_terraform_file(tf_file_name)
Expand Down Expand Up @@ -594,7 +608,7 @@ def _generate_streamquery_module(config):
"""
Generates .tf.json file for scheduled queries
"""
tf_file_name = os.path.join(TERRAFORM_FILES_PATH, 'scheduled_queries.tf.json')
tf_file_name = os.path.join(config.terraform_temp_path, 'scheduled_queries.tf.json')
if not config.get('scheduled_queries', {}).get('enabled', False):
remove_temp_terraform_file(tf_file_name)
return
Expand Down Expand Up @@ -637,7 +651,7 @@ def generate_global_lambda_settings(
)
raise ConfigError(message)

tf_tmp_file = os.path.join(TERRAFORM_FILES_PATH, '{}.tf.json'.format(tf_tmp_file_name))
tf_tmp_file = os.path.join(config.terraform_temp_path, '{}.tf.json'.format(tf_tmp_file_name))

if required and conf_name not in config['lambda']:
message = 'Required configuration missing in lambda.json: {}'.format(conf_name)
Expand Down
Loading