Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 34 additions & 25 deletions samcli/commands/pipeline/bootstrap/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
from samcli.lib.utils.version_checker import check_newer_version
from .guided_context import GuidedContext

SHORT_HELP = "Sets up infrastructure resources for AWS SAM CI/CD pipelines."

HELP_TEXT = """Sets up the following infrastructure resources for AWS SAM CI/CD pipelines:
\n\t - Pipeline IAM user with access key ID and secret access key credentials to be shared with the CI/CD system
\n\t - Pipeline execution IAM role assumed by the pipeline user to obtain access to the AWS account
\n\t - CloudFormation execution IAM role assumed by CloudFormation to deploy the AWS SAM application
\n\t - Artifacts S3 bucket to hold the AWS SAM build artifacts
\n\t - Optionally, an ECR image repository to hold container image Lambda deployment packages
SHORT_HELP = "Generates the necessary AWS resources to connect your CI/CD system."

HELP_TEXT = """
SAM Pipeline Bootstrap generates the necessary AWS resources to connect your
CI/CD system. This step must be completed for each pipeline stage prior to
running sam pipeline init
"""

PIPELINE_CONFIG_DIR = os.path.join(".aws-sam", "pipeline")
Expand All @@ -46,27 +44,24 @@
)
@click.option(
"--pipeline-user",
help="The ARN of the IAM user having its access key ID and secret access key shared with the CI/CD system. "
"It is used to grant this IAM user the permissions to access the corresponding AWS account. "
"If not provided, the command will create one along with access key ID and secret access key credentials.",
help="An IAM user generated or referenced by sam pipeline bootstrap in order to "
"allow the connected CI/CD system to connect to the SAM CLI.",
required=False,
)
@click.option(
"--pipeline-execution-role",
help="The ARN of an IAM role to be assumed by the pipeline user to operate on this environment. "
"Provide it only if you want to user your own role, otherwise, the command will create one",
help="Execution role that the CI/CD system assumes in order to make changes to resources on your behalf.",
required=False,
)
@click.option(
"--cloudformation-execution-role",
help="The ARN of an IAM role to be assumed by the CloudFormation service while deploying the application's stack. "
"Provide it only if you want to user your own role, otherwise, the command will create one.",
help="Execution role that CloudFormation assumes in order to make changes to resources on your behalf",
required=False,
)
@click.option(
"--artifacts-bucket",
help="The ARN of an S3 bucket to hold the AWS SAM build artifacts. "
"Provide it only if you want to user your own S3 bucket, otherwise, the command will create one.",
"--bucket",
help="The name of the S3 bucket where this command uploads your CloudFormation template. This is required for"
"deployments of templates sized greater than 51,200 bytes.",
required=False,
)
@click.option(
Expand All @@ -78,9 +73,7 @@
)
@click.option(
"--image-repository",
help="The ARN of an ECR image repository to hold the containers images of Lambda functions of Image package type. "
"If provided, the --create-image-repository argument is ignored. If not provided and --create-image-repository is "
"set to true, the command will create one.",
help="ECR repo uri where this command uploads the image artifacts that are referenced in your template.",
required=False,
)
@click.option(
Expand All @@ -107,7 +100,7 @@ def cli(
pipeline_user: Optional[str],
pipeline_execution_role: Optional[str],
cloudformation_execution_role: Optional[str],
artifacts_bucket: Optional[str],
bucket: Optional[str],
create_image_repository: bool,
image_repository: Optional[str],
pipeline_ip_range: Optional[str],
Expand All @@ -126,7 +119,7 @@ def cli(
pipeline_user_arn=pipeline_user,
pipeline_execution_role_arn=pipeline_execution_role,
cloudformation_execution_role_arn=cloudformation_execution_role,
artifacts_bucket_arn=artifacts_bucket,
artifacts_bucket_arn=bucket,
create_image_repository=create_image_repository,
image_repository_arn=image_repository,
pipeline_ip_range=pipeline_ip_range,
Expand All @@ -151,6 +144,7 @@ def do_cli(
confirm_changeset: bool,
config_file: Optional[str],
config_env: Optional[str],
standalone: bool = True,
) -> None:
"""
implementation of `sam pipeline bootstrap` command
Expand All @@ -159,7 +153,21 @@ def do_cli(
pipeline_user_arn = _load_saved_pipeline_user_arn()

if interactive:
if standalone:
click.echo(
dedent(
"""\

sam pipeline bootstrap generates the necessary AWS resources to connect a stage in
your CI/CD system. We will ask for [1] stage definition, [2] account details, and
[3] references to existing resources in order to bootstrap these pipeline
resources. You can also add optional security parameters.
"""
),
)

guided_context = GuidedContext(
profile=profile,
environment_name=environment_name,
pipeline_user_arn=pipeline_user_arn,
pipeline_execution_role_arn=pipeline_execution_role_arn,
Expand All @@ -180,6 +188,7 @@ def do_cli(
create_image_repository = guided_context.create_image_repository
image_repository_arn = guided_context.image_repository_arn
region = guided_context.region
profile = guided_context.profile

if not environment_name:
raise click.UsageError("Missing required parameter '--environment'")
Expand Down Expand Up @@ -210,8 +219,8 @@ def do_cli(
dedent(
f"""\
View the definition in {os.path.join(PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME)},
run {Colored().bold("sam pipeline bootstrap")} to generate another set of resources, or proceed to
{Colored().bold("sam pipeline init")} to create your pipeline configuration file.
run sam pipeline bootstrap to generate another set of resources, or proceed to
sam pipeline init to create your pipeline configuration file.
"""
)
)
Expand Down
104 changes: 72 additions & 32 deletions samcli/commands/pipeline/bootstrap/guided_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@
An interactive flow that prompt the user for required information to bootstrap the AWS account of an environment
with the required infrastructure
"""
import os
import sys
from textwrap import dedent
from typing import Optional, List, Tuple, Callable

import click
from botocore.credentials import EnvProvider

from samcli.commands.exceptions import CredentialsError
from samcli.commands.pipeline.external_links import CONFIG_AWS_CRED_DOC_URL
from samcli.lib.bootstrap.bootstrap import get_current_account_id
from samcli.lib.utils.colors import Colored

from samcli.lib.utils.defaults import get_default_aws_region
from samcli.lib.utils.profile import list_available_profiles


class GuidedContext:
def __init__(
self,
profile: Optional[str] = None,
environment_name: Optional[str] = None,
pipeline_user_arn: Optional[str] = None,
pipeline_execution_role_arn: Optional[str] = None,
Expand All @@ -28,6 +33,7 @@ def __init__(
pipeline_ip_range: Optional[str] = None,
region: Optional[str] = None,
) -> None:
self.profile = profile
self.environment_name = environment_name
self.pipeline_user_arn = pipeline_user_arn
self.pipeline_execution_role_arn = pipeline_execution_role_arn
Expand All @@ -39,10 +45,47 @@ def __init__(
self.region = region
self.color = Colored()

def _prompt_account_id(self) -> None:
profiles = list_available_profiles()
click.echo("The following AWS credential sources are available to use:")
click.echo(
dedent(
f"""\
To know more about configuration AWS credentials, visit the link below:
{CONFIG_AWS_CRED_DOC_URL}\
"""
)
)
if os.getenv(EnvProvider.ACCESS_KEY) and os.getenv(EnvProvider.SECRET_KEY):
click.echo(f" e. Environment variables: {EnvProvider.ACCESS_KEY} and {EnvProvider.SECRET_KEY}")
for i, profile in enumerate(profiles):
click.echo(f" {i + 1}. {profile} (named profile)")
click.echo(" q. Quit and configure AWS credential myself")
answer = click.prompt(
"Select an account source to associate with this stage",
show_choices=False,
show_default=False,
type=click.Choice([str(i + 1) for i in range(len(profiles))] + ["q", "e"]),
)
if answer == "q":
sys.exit(0)
elif answer == "e":
# by default, env variable has higher precedence
# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html#envvars-list
self.profile = None
else:
self.profile = profiles[int(answer) - 1]

try:
account_id = get_current_account_id(self.profile)
click.echo(self.color.green(f"Associated account {account_id} with stage {self.environment_name}."))
except CredentialsError as ex:
click.echo(self.color.red(ex.message))
self._prompt_account_id()

def _prompt_stage_name(self) -> None:
click.echo(
"Enter a name for the stage you want to bootstrap. This will be referenced later "
"when generating a Pipeline Config File with Pipeline Init."
"Enter a name for this stage. This will be referenced later when you use the sam pipeline init command:"
)
self.environment_name = click.prompt(
"Stage name",
Expand All @@ -52,7 +95,7 @@ def _prompt_stage_name(self) -> None:

def _prompt_region_name(self) -> None:
self.region = click.prompt(
"Enter the region you want these resources to create",
"Enter the region in which you want these resources to be created",
type=click.STRING,
default=get_default_aws_region(),
)
Expand Down Expand Up @@ -92,7 +135,7 @@ def _prompt_image_repository(self) -> None:
if click.confirm("Does your application contain any IMAGE type Lambda functions?"):
self.image_repository_arn = click.prompt(
"Please enter the ECR image repository ARN(s) for your Image type function(s)."
"If you do not yet have a repostiory, we will create one for you",
"If you do not yet have a repository, we will create one for you",
default="",
type=click.STRING,
)
Expand All @@ -110,36 +153,37 @@ def _prompt_ip_range(self) -> None:

def _get_user_inputs(self) -> List[Tuple[str, Callable[[], None]]]:
return [
(f"Account: {get_current_account_id(self.profile)}", self._prompt_account_id),
(f"Stage name: {self.environment_name}", self._prompt_stage_name),
(f"Region: {self.region}", self._prompt_region_name),
(
f"Pipeline user ARN: {self.pipeline_user_arn}"
if self.pipeline_user_arn
else "Pipeline user: to be created",
else "Pipeline user: [to be created]",
self._prompt_pipeline_user,
),
(
f"Pipeline execution role ARN: {self.pipeline_execution_role_arn}"
if self.pipeline_execution_role_arn
else "Pipeline execution role: to be created",
else "Pipeline execution role: [to be created]",
self._prompt_pipeline_execution_role,
),
(
f"CloudFormation execution role ARN: {self.cloudformation_execution_role_arn}"
if self.cloudformation_execution_role_arn
else "CloudFormation execution role: to be created",
else "CloudFormation execution role: [to be created]",
self._prompt_cloudformation_execution_role,
),
(
f"Artifacts bucket ARN: {self.artifacts_bucket_arn}"
if self.artifacts_bucket_arn
else "Artifacts bucket: to be created",
else "Artifacts bucket: [to be created]",
self._prompt_artifacts_bucket,
),
(
f"ECR image repository ARN: {self.image_repository_arn}"
if self.image_repository_arn
else f"ECR image repository: {'to be created' if self.create_image_repository else 'skipped'}",
else f"ECR image repository: [{'to be created' if self.create_image_repository else 'skipped'}]",
self._prompt_image_repository,
),
(
Expand All @@ -156,71 +200,66 @@ def run(self) -> None: # pylint: disable=too-many-branches
for the pipeline to work. Users can provide all, none or some resources' ARNs and leave the remaining empty
and it will be created by the bootstrap command
"""
click.secho(
dedent(
f"""\
{Colored().bold("sam pipeline bootstrap")} generates the necessary AWS resources to connect your
CI/CD system. We will ask for [1] account details, [2] stage definition,
and [3] references to existing resources in order to bootstrap these pipeline
resources. You can also add optional security parameters.
"""
),
fg="cyan",
)

account_id = get_current_account_id()
click.secho("[1] Account details", bold=True)
if click.confirm(f"You are bootstrapping resources in account {account_id}. Do you want to switch accounts?"):
click.echo(f"Please refer to this page about configuring credentials: {CONFIG_AWS_CRED_DOC_URL}.")
sys.exit(0)

click.secho("[2] Stage definition", bold=True)
click.secho(self.color.bold("[1] Stage definition"))
if self.environment_name:
click.echo(f"Stage name: {self.environment_name}")
else:
self._prompt_stage_name()
click.echo()

click.secho(self.color.bold("[2] Account details"))
self._prompt_account_id()
click.echo()

if not self.region:
self._prompt_region_name()

click.secho("[3] Reference existing resources", bold=True)
if self.pipeline_user_arn:
click.echo(f"Pipeline IAM user ARN: {self.pipeline_user_arn}")
else:
self._prompt_pipeline_user()
click.echo()

click.secho(self.color.bold("[3] Reference application build resources"))

if self.pipeline_execution_role_arn:
click.echo(f"Pipeline execution role ARN: {self.pipeline_execution_role_arn}")
else:
self._prompt_pipeline_execution_role()
click.echo()

if self.cloudformation_execution_role_arn:
click.echo(f"CloudFormation execution role ARN: {self.cloudformation_execution_role_arn}")
else:
self._prompt_cloudformation_execution_role()
click.echo()

if self.artifacts_bucket_arn:
click.echo(f"Artifacts bucket ARN: {self.cloudformation_execution_role_arn}")
else:
self._prompt_artifacts_bucket()
click.echo()

if self.image_repository_arn:
click.echo(f"ECR image repository ARN: {self.image_repository_arn}")
else:
self._prompt_image_repository()
click.echo()

click.secho("[4] Security definition - OPTIONAL", bold=True)
click.secho(self.color.bold("[4] Security definition - OPTIONAL"))
if self.pipeline_ip_range:
click.echo(f"Pipeline IP address range: {self.pipeline_ip_range}")
else:
self._prompt_ip_range()
click.echo()

# Ask customers to confirm the inputs
click.secho(self.color.bold("[5] Summary"))
while True:
inputs = self._get_user_inputs()
click.secho(self.color.cyan("Below is the summary of the answers:"))
click.secho("Below is the summary of the answers:")
for i, (text, _) in enumerate(inputs):
click.secho(self.color.cyan(f" {i + 1}. {text}"))
click.secho(f" {i + 1}. {text}")
edit_input = click.prompt(
text="Press enter to confirm the values above, or select an item to edit the value",
default="0",
Expand All @@ -230,5 +269,6 @@ def run(self) -> None: # pylint: disable=too-many-branches
)
if int(edit_input):
inputs[int(edit_input) - 1][1]()
click.echo()
else:
break
Loading