Skip to content

Commit

Permalink
Merge remote-tracking branch 'aws-sam-cli-public/develop-terraform-ho…
Browse files Browse the repository at this point in the history
…oks' into develop

# Conflicts:
#	samcli/commands/_utils/experimental.py
  • Loading branch information
moelasmar committed Sep 1, 2022
2 parents 074502f + f717ef0 commit c7d28da
Show file tree
Hide file tree
Showing 94 changed files with 7,151 additions and 251 deletions.
8 changes: 8 additions & 0 deletions appveyor-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ install:
# get testing env vars
- sh: "sudo apt install -y jq"

# install Terraform
- sh: "sudo apt update"
- sh: "TER_VER=`curl -s https://api.github.com/repos/hashicorp/terraform/releases/latest | grep tag_name | cut -d: -f2 | tr -d \\\"\\,\\v | awk '{$1=$1};1'`"
- sh: "wget https://releases.hashicorp.com/terraform/${TER_VER}/terraform_${TER_VER}_linux_amd64.zip -P /tmp"
- sh: "sudo unzip -d /opt/terraform /tmp/terraform_${TER_VER}_linux_amd64.zip"
- sh: "sudo mv /opt/terraform/terraform /usr/local/bin/"
- sh: "terraform -version"

- sh: "python3.9 -m venv .venv_env_vars"
- sh: ".venv_env_vars/bin/pip install boto3"
- sh: "test_env_var=$(.venv_env_vars/bin/python tests/get_testing_resources.py)"
Expand Down
4 changes: 4 additions & 0 deletions appveyor-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ install:
- "python --version"
- "docker info"

# install Terraform CLI
- "choco install terraform"
- "terraform -version"

# Upgrade setuptools, wheel and virtualenv
- "python -m pip install --upgrade setuptools wheel virtualenv"

Expand Down
12 changes: 12 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ for:
- "python -m pip install --upgrade setuptools wheel virtualenv"
- "docker info"

# install Terraform CLI
- "choco install terraform"
- "terraform -version"

# Install AWS CLI Globally outside of a venv.
- "pip install awscli"

Expand Down Expand Up @@ -141,6 +145,14 @@ for:
- sh: "sudo unzip -d /opt/gradle /tmp/gradle-*.zip"
- sh: "PATH=/opt/gradle/gradle-5.5/bin:$PATH"

# install Terraform
- sh: "sudo apt update"
- sh: "TER_VER=`curl -s https://api.github.com/repos/hashicorp/terraform/releases/latest | grep tag_name | cut -d: -f2 | tr -d \\\"\\,\\v | awk '{$1=$1};1'`"
- sh: "wget https://releases.hashicorp.com/terraform/${TER_VER}/terraform_${TER_VER}_linux_amd64.zip -P /tmp"
- sh: "sudo unzip -d /opt/terraform /tmp/terraform_${TER_VER}_linux_amd64.zip"
- sh: "sudo mv /opt/terraform/terraform /usr/local/bin/"
- sh: "terraform -version"

# Install AWS CLI
- sh: "virtualenv aws_cli"
- sh: "./aws_cli/bin/python -m pip install awscli"
Expand Down
5 changes: 4 additions & 1 deletion installer/pyinstaller/hook-samcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
hiddenimports = SAM_CLI_HIDDEN_IMPORTS

datas = (
hooks.collect_data_files("samcli")
hooks.collect_all(
"samcli", include_py_files=True, include_datas=["hook_packages/terraform/copy_terraform_built_artifacts.py"]
)[0]
+ hooks.collect_data_files("samcli")
+ hooks.collect_data_files("samtranslator")
+ hooks.collect_data_files("aws_lambda_builders")
+ hooks.collect_data_files("text_unidecode")
Expand Down
2 changes: 1 addition & 1 deletion samcli/commands/_utils/command_exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from botocore.exceptions import NoRegionError, ClientError

from samcli.commands._utils.options import parameterized_option
from samcli.commands._utils.parameterized_option import parameterized_option
from samcli.commands.exceptions import CredentialsError, RegionError
from samcli.lib.utils.boto_utils import get_client_error_code

Expand Down
10 changes: 10 additions & 0 deletions samcli/commands/_utils/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
SAM CLI Default Build constants
"""
import os

DEFAULT_STACK_NAME = "sam-app"
DEFAULT_BUILD_DIR = os.path.join(".aws-sam", "build")
DEFAULT_BUILD_DIR_WITH_AUTO_DEPENDENCY_LAYER = os.path.join(".aws-sam", "auto-dependency-layer")
DEFAULT_CACHE_DIR = os.path.join(".aws-sam", "cache")
DEFAULT_BUILT_TEMPLATE_PATH = os.path.join(".aws-sam", "build", "template.yaml")
118 changes: 118 additions & 0 deletions samcli/commands/_utils/custom_options/hook_package_id_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
Custom Click options for hook package id
"""

import logging
import os
import click

from samcli.commands._utils.constants import DEFAULT_BUILT_TEMPLATE_PATH
from samcli.commands._utils.experimental import prompt_experimental, ExperimentalFlag
from samcli.lib.hook.exceptions import InvalidHookWrapperException
from samcli.lib.hook.hook_wrapper import IacHookWrapper, get_available_hook_packages_ids

LOG = logging.getLogger(__name__)


class HookPackageIdOption(click.Option):
"""
A custom option class that allows do custom validation for the SAM CLI commands options in case if hook package
id is passed. It also calls the correct IaC prepare hook, and update the SAM CLI commands options based on the
prepare hook output.
"""

def __init__(self, *args, **kwargs):
self._force_prepare = kwargs.pop("force_prepare", True)
self._invalid_coexist_options = kwargs.pop("invalid_coexist_options", [])
super().__init__(*args, **kwargs)

def handle_parse_result(self, ctx, opts, args):
opt_name = self.name.replace("_", "-")
if self.name in opts:
command_name = ctx.command.name
if command_name in ["invoke", "start-lambda", "start-api"]:
command_name = f"local {command_name}"
# validate the hook_package_id value exists
hook_package_id = opts[self.name]
iac_hook_wrapper = None
try:
iac_hook_wrapper = IacHookWrapper(hook_package_id)
except InvalidHookWrapperException as e:
raise click.BadParameter(
f"{hook_package_id} is not a valid hook package id. This is the list of valid "
f"packages ids {get_available_hook_packages_ids()}"
) from e

# validate coexist options
for invalid_opt in self._invalid_coexist_options:
invalid_opt_name = invalid_opt.replace("-", "_")
if invalid_opt_name in opts:
raise click.BadParameter(
f"Parameters {opt_name}, and {','.join(self._invalid_coexist_options)} can not be used together"
)
# check beta-feature
beta_features = opts.get("beta_features")

# check if beta feature flag is required for a specific hook package
# The IaCs support experimental flag map will contain only the beta IaCs. In case we support the external
# hooks, we need to first know that the hook package is an external, and to handle the beta feature of it
# using different approach
experimental_entry = ExperimentalFlag.IaCsSupport.get(hook_package_id)
if beta_features is None and experimental_entry is not None:

iac_support_message = _get_iac_support_experimental_prompt_message(hook_package_id, command_name)
if not prompt_experimental(experimental_entry, iac_support_message):
LOG.debug("Experimental flag is disabled and prepare hook is not run")
return super().handle_parse_result(ctx, opts, args)
elif not beta_features:
LOG.debug("beta-feature flag is disabled and prepare hook is not run")
return super().handle_parse_result(ctx, opts, args)

# call prepare hook
built_template_path = DEFAULT_BUILT_TEMPLATE_PATH
if not self._force_prepare and os.path.exists(built_template_path):
LOG.info("Skip Running Prepare hook. The current application is already prepared.")
else:
LOG.info("Running Prepare Hook to prepare the current application")

iac_project_path = os.getcwd()
output_dir_path = os.path.join(iac_project_path, ".aws-sam", "iacs_metadata")
if not os.path.exists(output_dir_path):
os.makedirs(output_dir_path, exist_ok=True)
debug = opts.get("debug", False)
aws_profile = opts.get("profile")
aws_region = opts.get("region")
metadata_file = iac_hook_wrapper.prepare(
output_dir_path, iac_project_path, debug, aws_profile, aws_region
)

LOG.info("Prepare Hook is done, and metadata file generated at %s", metadata_file)
opts["template_file"] = metadata_file
return super().handle_parse_result(ctx, opts, args)


def _get_iac_support_experimental_prompt_message(hook_package_id: str, command: str) -> str:
"""
return the customer prompt message for a specific hook package.
Parameters
----------
hook_package_id: str
the hook package id to determine what is the supported iac
command: str
the current sam command
Returns
-------
str
the customer prompt message for a specific IaC.
"""

supported_iacs_messages = {
"terraform": (
"Supporting Terraform applications is a beta feature.\n"
"Please confirm if you would like to proceed using SAM CLI with terraform application.\n"
f"You can also enable this beta feature with 'sam {command} --beta-features'."
)
}
return supported_iacs_messages.get(hook_package_id, "")
18 changes: 16 additions & 2 deletions samcli/commands/_utils/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from samcli.cli.context import Context

from samcli.cli.global_config import ConfigEntry, GlobalConfig
from samcli.commands._utils.options import parameterized_option
from samcli.commands._utils.parameterized_option import parameterized_option
from samcli.lib.utils.colors import Colored

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -46,6 +46,11 @@ class ExperimentalFlag:
BuildPerformance = ExperimentalEntry(
"experimentalBuildPerformance", EXPERIMENTAL_ENV_VAR_PREFIX + "BUILD_PERFORMANCE"
)
IaCsSupport = {
"terraform": ExperimentalEntry(
"experimentalTerraformSupport", EXPERIMENTAL_ENV_VAR_PREFIX + "TERRAFORM_SUPPORT"
)
}


def is_experimental_enabled(config_entry: ExperimentalEntry) -> bool:
Expand Down Expand Up @@ -90,7 +95,16 @@ def get_all_experimental() -> List[ExperimentalEntry]:
List[ExperimentalEntry]
List all experimental flags in the ExperimentalFlag class.
"""
return [getattr(ExperimentalFlag, name) for name in dir(ExperimentalFlag) if not name.startswith("__")]
all_experimental_flags = []
for name in dir(ExperimentalFlag):
if name.startswith("__"):
continue
value = getattr(ExperimentalFlag, name)
if isinstance(value, ExperimentalEntry):
all_experimental_flags.append(value)
elif isinstance(value, dict):
all_experimental_flags += value.values()
return all_experimental_flags


def get_all_experimental_statues() -> Dict[str, bool]:
Expand Down
72 changes: 29 additions & 43 deletions samcli/commands/_utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
import os
import logging
from functools import partial
import types

import click
from click.types import FuncParamType

from samcli.commands._utils.constants import (
DEFAULT_STACK_NAME,
DEFAULT_BUILD_DIR,
DEFAULT_CACHE_DIR,
DEFAULT_BUILT_TEMPLATE_PATH,
)
from samcli.commands._utils.custom_options.hook_package_id_option import HookPackageIdOption
from samcli.commands._utils.parameterized_option import parameterized_option
from samcli.commands._utils.template import get_template_data, TemplateNotFoundException
from samcli.cli.types import (
CfnParameterOverridesType,
Expand All @@ -21,55 +28,15 @@
)
from samcli.commands._utils.custom_options.option_nargs import OptionNargs
from samcli.commands._utils.template import get_template_artifacts_format
from samcli.lib.hook.hook_wrapper import get_available_hook_packages_ids
from samcli.lib.observability.util import OutputOption
from samcli.lib.utils.packagetype import ZIP, IMAGE

_TEMPLATE_OPTION_DEFAULT_VALUE = "template.[yaml|yml|json]"
DEFAULT_STACK_NAME = "sam-app"
DEFAULT_BUILD_DIR = os.path.join(".aws-sam", "build")
DEFAULT_BUILD_DIR_WITH_AUTO_DEPENDENCY_LAYER = os.path.join(".aws-sam", "auto-dependency-layer")
DEFAULT_CACHE_DIR = os.path.join(".aws-sam", "cache")

LOG = logging.getLogger(__name__)


def parameterized_option(option):
"""Meta decorator for option decorators.
This adds the ability to specify optional parameters for option decorators.
Usage:
@parameterized_option
def some_option(f, required=False)
...
@some_option
def command(...)
or
@some_option(required=True)
def command(...)
"""

def parameter_wrapper(*args, **kwargs):
if len(args) == 1 and isinstance(args[0], types.FunctionType):
# Case when option decorator does not have parameter
# @stack_name_option
# def command(...)
return option(args[0])

# Case when option decorator does have parameter
# @stack_name_option("a", "b")
# def command(...)

def option_wrapper(f):
return option(f, *args, **kwargs)

return option_wrapper

return parameter_wrapper


def get_or_default_template_file_name(ctx, param, provided_value, include_build):
"""
Default value for the template file name option is more complex than what Click can handle.
Expand All @@ -88,7 +55,7 @@ def get_or_default_template_file_name(ctx, param, provided_value, include_build)
search_paths = ["template.yaml", "template.yml", "template.json"]

if include_build:
search_paths.insert(0, os.path.join(".aws-sam", "build", "template.yaml"))
search_paths.insert(0, DEFAULT_BUILT_TEMPLATE_PATH)

if provided_value == _TEMPLATE_OPTION_DEFAULT_VALUE:
# "--template" is an alias of "--template-file", however, only the first option name "--template-file" in
Expand Down Expand Up @@ -680,6 +647,25 @@ def resolve_s3_click_option(guided):
)


def hook_package_id_click_option(force_prepare=True, invalid_coexist_options=None):
"""
Click Option for hook-package-id option
"""
return click.option(
"--hook-package-id",
default=None,
type=click.STRING,
cls=HookPackageIdOption,
required=False,
is_eager=True,
force_prepare=force_prepare,
invalid_coexist_options=invalid_coexist_options if invalid_coexist_options else [],
help=f"The id of the hook package to be used to extend the SAM CLI commands functionality. As an example, you "
f"can use `terraform` to extend SAM CLI commands functionality to support terraform applications. "
f"Available Hook Packages Ids {get_available_hook_packages_ids()}",
)


@parameterized_option
def resolve_s3_option(f, guided=False):
return resolve_s3_click_option(guided)(f)
Expand Down
42 changes: 42 additions & 0 deletions samcli/commands/_utils/parameterized_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Parameterized Option Class
"""

import types


def parameterized_option(option):
"""Meta decorator for option decorators.
This adds the ability to specify optional parameters for option decorators.
Usage:
@parameterized_option
def some_option(f, required=False)
...
@some_option
def command(...)
or
@some_option(required=True)
def command(...)
"""

def parameter_wrapper(*args, **kwargs):
if len(args) == 1 and isinstance(args[0], types.FunctionType):
# Case when option decorator does not have parameter
# @stack_name_option
# def command(...)
return option(args[0])

# Case when option decorator does have parameter
# @stack_name_option("a", "b")
# def command(...)

def option_wrapper(f):
return option(f, *args, **kwargs)

return option_wrapper

return parameter_wrapper
Loading

0 comments on commit c7d28da

Please sign in to comment.