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

feat: Added --skip-prepare-infra to skip the prepare hook stage #4377

Merged
merged 16 commits into from
Nov 5, 2022
Merged
Show file tree
Hide file tree
Changes from 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,15 @@ def _call_prepare_hook(self, iac_hook_wrapper, opts):
output_dir_path = os.path.join(iac_project_path, ".aws-sam-iacs", "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)
skip_prepare_infra = opts.get("skip_prepare_infra", False)

metadata_file = iac_hook_wrapper.prepare(
output_dir_path, iac_project_path, debug, aws_profile, aws_region, skip_prepare_infra
)

LOG.info("Prepare hook completed and metadata file generated at: %s", metadata_file)
opts["template_file"] = metadata_file
Expand Down
39 changes: 39 additions & 0 deletions samcli/commands/_utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,26 @@ def resolve_s3_callback(ctx, param, provided_value, artifact, exc_set, exc_not_s
return provided_value


def skip_prepare_infra_callback(ctx, param, provided_value):
"""
Callback for --skip-prepare-infra to check if --hook-package-id is also specified

Parameters
----------
ctx: click.core.Context
Click context
param: click.Option
Parameter properties
provided_value: bool
True if option was provided
"""
is_option_provided = provided_value or ctx.default_map.get("skip_prepare_infra")
is_hook_provided = ctx.params.get("hook_package_id") or ctx.default_map.get("hook_package_id")

if is_option_provided and not is_hook_provided:
raise click.BadOptionUsage(option_name=param.name, ctx=ctx, message="Missing option --hook-package-id")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--hook-package-id to --hook-name



def template_common_option(f):
"""
Common ClI option for template
Expand Down Expand Up @@ -693,6 +713,25 @@ def decorator(f):
return composed_decorator(decorator_list)


def skip_prepare_infra_click_option():
"""
Click option to skip the hook preparation stage
"""
return click.option(
"--skip-prepare-infra",
is_flag=True,
required=False,
callback=skip_prepare_infra_callback,
help="This option should be used to skip the preparation stage if there "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subject to change.

"have not been any infrastructure changes. The --hook-name option should "
"also be specified when using this option.",
)


def skip_prepare_infra_option(f):
return skip_prepare_infra_click_option()(f)


@parameterized_option
def resolve_s3_option(f, guided=False):
return resolve_s3_click_option(guided)(f)
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/build/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from samcli.cli.context import Context
from samcli.commands._utils.experimental import experimental, ExperimentalFlag, is_experimental_enabled
from samcli.commands._utils.options import (
skip_prepare_infra_option,
template_option_without_build,
docker_common_options,
parameter_override_option,
Expand Down Expand Up @@ -85,6 +86,8 @@
force_prepare=True,
invalid_coexist_options=["t", "template-file", "template", "parameter-overrides"],
)
@configuration_option(provider=TomlProvider(section="parameters"))
@skip_prepare_infra_option
@use_container_build_option
@click.option(
"--container-env-var",
Expand Down Expand Up @@ -172,6 +175,7 @@ def cli(
config_file: str,
config_env: str,
hook_package_id: Optional[str],
skip_prepare_infra: bool,
) -> None:
"""
`sam build` command entry point
Expand Down
5 changes: 4 additions & 1 deletion samcli/commands/local/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options, print_cmdline_args
from samcli.commands._utils.experimental import experimental, is_experimental_enabled, ExperimentalFlag
from samcli.commands._utils.options import hook_package_id_click_option
from samcli.commands._utils.options import hook_package_id_click_option, skip_prepare_infra_option
from samcli.commands.local.cli_common.options import invoke_common_options, local_common_options
from samcli.commands.local.lib.exceptions import InvalidIntermediateImageError
from samcli.lib.telemetry.metric import track_command
Expand Down Expand Up @@ -40,6 +40,8 @@
@hook_package_id_click_option(
force_prepare=False, invalid_coexist_options=["t", "template-file", "template", "parameter-overrides"]
)
@configuration_option(provider=TomlProvider(section="parameters"))
@skip_prepare_infra_option
@click.option(
"--event",
"-e",
Expand Down Expand Up @@ -83,6 +85,7 @@ def cli(
container_host_interface,
invoke_image,
hook_package_id,
skip_prepare_infra,
):
"""
`sam local invoke` command entry point
Expand Down
5 changes: 4 additions & 1 deletion samcli/commands/local/start_lambda/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options, print_cmdline_args
from samcli.commands._utils.experimental import experimental, is_experimental_enabled, ExperimentalFlag
from samcli.commands._utils.options import hook_package_id_click_option
from samcli.commands._utils.options import hook_package_id_click_option, skip_prepare_infra_option
from samcli.commands.local.cli_common.options import (
invoke_common_options,
service_common_options,
Expand Down Expand Up @@ -64,6 +64,8 @@
@hook_package_id_click_option(
force_prepare=False, invalid_coexist_options=["t", "template-file", "template", "parameter-overrides"]
)
@configuration_option(provider=TomlProvider(section="parameters"))
@skip_prepare_infra_option
@service_common_options(3001)
@invoke_common_options
@experimental
Expand Down Expand Up @@ -103,6 +105,7 @@ def cli(
container_host_interface,
invoke_image,
hook_package_id,
skip_prepare_infra,
):
"""
`sam local start-lambda` command entry point
Expand Down
124 changes: 65 additions & 59 deletions samcli/hook_packages/terraform/hooks/prepare/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
PropertyBuilder = Callable[[dict, TFResource], Any]
PropertyBuilderMapping = Dict[str, PropertyBuilder]

TERRAFORM_METADATA_FILE = "template.json"
TERRAFORM_BUILD_SCRIPT = "copy_terraform_built_artifacts.py"
TF_BACKEND_OVERRIDE_FILENAME = "z_samcli_backend_override"

Expand Down Expand Up @@ -126,68 +127,73 @@ def prepare(params: dict) -> dict:
output_dir_path = os.path.normpath(os.path.join(terraform_application_dir, output_dir_path))
LOG.debug("The normalized OutputDirPath value is %s", output_dir_path)

try:
# initialize terraform application
LOG.info("Initializing Terraform application")
run(["terraform", "init", "-input=false"], check=True, capture_output=True, cwd=terraform_application_dir)

# get json output of terraform plan
LOG.info("Creating terraform plan and getting JSON output")

with osutils.tempfile_platform_independent() as temp_file:
run(
# input false to avoid SAM CLI to stuck in case if the Terraform project expects input, and customer
# does not provide it.
["terraform", "plan", "-out", temp_file.name, "-input=false"],
check=True,
capture_output=True,
cwd=terraform_application_dir,
)
result = run(
["terraform", "show", "-json", temp_file.name],
check=True,
capture_output=True,
cwd=terraform_application_dir,
)
tf_json = json.loads(result.stdout)

# convert terraform to cloudformation
LOG.info("Generating metadata file")
cfn_dict = _translate_to_cfn(tf_json, output_dir_path, terraform_application_dir)

if cfn_dict.get("Resources"):
_update_resources_paths(cfn_dict.get("Resources"), terraform_application_dir) # type: ignore
skip_prepare_infra = params.get("SkipPrepareInfra")
metadata_file_path = os.path.join(output_dir_path, TERRAFORM_METADATA_FILE)

# store in supplied output dir
if not os.path.exists(output_dir_path):
os.makedirs(output_dir_path, exist_ok=True)
metadataFilePath = os.path.join(output_dir_path, "template.json")
LOG.info("Finished generating metadata file. Storing in %s", metadataFilePath)
with open(metadataFilePath, "w+") as metadata_file:
json.dump(cfn_dict, metadata_file)

return {"iac_applications": {"MainApplication": {"metadata_file": metadataFilePath}}}

except CalledProcessError as e:
stderr_output = str(e.stderr)

# stderr can take on bytes or just be a plain string depending on terminal
if isinstance(e.stderr, bytes):
stderr_output = e.stderr.decode("utf-8")
if skip_prepare_infra and os.path.exists(metadata_file_path):
LOG.info("Skipping preparation stage, the metadata file already exists at %s", metadata_file_path)
else:
try:
# initialize terraform application
LOG.info("Initializing Terraform application")
run(["terraform", "init", "-input=false"], check=True, capture_output=True, cwd=terraform_application_dir)

# get json output of terraform plan
LOG.info("Creating terraform plan and getting JSON output")

with osutils.tempfile_platform_independent() as temp_file:
run(
# input false to avoid SAM CLI to stuck in case if the Terraform project expects input, and customer
# does not provide it.
["terraform", "plan", "-out", temp_file.name, "-input=false"],
check=True,
capture_output=True,
cwd=terraform_application_dir,
)
result = run(
["terraform", "show", "-json", temp_file.name],
check=True,
capture_output=True,
cwd=terraform_application_dir,
)
tf_json = json.loads(result.stdout)

# convert terraform to cloudformation
LOG.info("Generating metadata file")
cfn_dict = _translate_to_cfn(tf_json, output_dir_path, terraform_application_dir)

if cfn_dict.get("Resources"):
_update_resources_paths(cfn_dict.get("Resources"), terraform_application_dir) # type: ignore

# store in supplied output dir
if not os.path.exists(output_dir_path):
os.makedirs(output_dir_path, exist_ok=True)

LOG.info("Finished generating metadata file. Storing in %s", metadata_file_path)
with open(metadata_file_path, "w+") as metadata_file:
json.dump(cfn_dict, metadata_file)
except CalledProcessError as e:
stderr_output = str(e.stderr)

# stderr can take on bytes or just be a plain string depending on terminal
if isinstance(e.stderr, bytes):
stderr_output = e.stderr.decode("utf-8")

# one of the subprocess.run calls resulted in non-zero exit code or some OS error
LOG.debug(
"Error running terraform command: \n" "cmd: %s \n" "stdout: %s \n" "stderr: %s \n",
e.cmd,
e.stdout,
stderr_output,
)

# one of the subprocess.run calls resulted in non-zero exit code or some OS error
LOG.debug(
"Error running terraform command: \n" "cmd: %s \n" "stdout: %s \n" "stderr: %s \n",
e.cmd,
e.stdout,
stderr_output,
)
raise PrepareHookException(
f"There was an error while preparing the Terraform application.\n{stderr_output}"
) from e
except OSError as e:
raise PrepareHookException(f"OSError: {e}") from e

raise PrepareHookException(
f"There was an error while preparing the Terraform application.\n{stderr_output}"
) from e
except OSError as e:
raise PrepareHookException(f"OSError: {e}") from e
return {"iac_applications": {"MainApplication": {"metadata_file": metadata_file_path}}}


def _update_resources_paths(cfn_resources: Dict[str, Any], terraform_application_dir: str) -> None:
Expand Down
4 changes: 4 additions & 0 deletions samcli/lib/hook/hook_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def prepare(
debug: bool = False,
aws_profile: Optional[str] = None,
aws_region: Optional[str] = None,
skip_prepare_infra: bool = False,
) -> str:
"""
Run the prepare hook to generate the IaC Metadata file.
Expand All @@ -67,6 +68,8 @@ def prepare(
AWS profile to use. Default is None (use default profile)
aws_region: str
AWS region to use. Default is None (use default region)
skip_prepare_infra: bool
Flag to skip skip prepare hook if we already have the metadata file. Default is False.

Returns
-------
Expand All @@ -78,6 +81,7 @@ def prepare(
"IACProjectPath": iac_project_path if iac_project_path else str(Path.cwd()),
"OutputDirPath": output_dir_path,
"Debug": debug,
"SkipPrepareInfra": skip_prepare_infra,
}
if aws_profile:
params["Profile"] = aws_profile
Expand Down
Loading