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

sam pipeline bootstrap #2811

Merged
merged 32 commits into from
Apr 24, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
83fc54a
two-stages-pipeline plugin
elbayaaa Mar 25, 2021
9f0147a
typos
elbayaaa Mar 31, 2021
3946009
add docstring
elbayaaa Mar 31, 2021
d3db05b
make mypy happy
elbayaaa Mar 31, 2021
201de2e
removing swap file
elbayaaa Mar 31, 2021
1d2cc06
delete the two_stages_pipeline plugin as the pipeline-bootstrap comma…
elbayaaa Apr 14, 2021
588d4ab
remove 'get_template_function_runtimes' function as the decision is m…
elbayaaa Apr 14, 2021
46402f0
sam pipeline bootstrap command
elbayaaa Apr 11, 2021
9747413
move the pipelineconfig.toml file to .aws-sam
elbayaaa Apr 15, 2021
8a7404e
UX - rewriting
elbayaaa Apr 16, 2021
6e3f21d
UX improvements
elbayaaa Apr 16, 2021
58e48a2
make black happy
elbayaaa Apr 17, 2021
206b7e3
apply review comments
elbayaaa Apr 19, 2021
23148ff
UX - rewriting
elbayaaa Apr 19, 2021
37a4f5f
refactor
elbayaaa Apr 19, 2021
8ea4153
Apply review comments
elbayaaa Apr 19, 2021
99c91c8
use python way of array elements assignments
elbayaaa Apr 19, 2021
b4c248f
Update samcli/lib/pipeline/bootstrap/stage.py
elbayaaa Apr 20, 2021
3acea8c
apply review comments
elbayaaa Apr 20, 2021
99bef0b
typo
elbayaaa Apr 20, 2021
eacfe9c
read using utf-8
elbayaaa Apr 20, 2021
5fdd32a
create and user a safe version of the save_config method
elbayaaa Apr 20, 2021
83712cd
apply review comments
elbayaaa Apr 20, 2021
2c3ad8b
rename _get_command_name to _get_command_names
elbayaaa Apr 20, 2021
d184e16
don't save generated ARNs for now, will save during init
elbayaaa Apr 20, 2021
6d9fb34
Revert "don't save generated ARNs for now, will save during init"
elbayaaa Apr 20, 2021
0f1152c
Notify the user to rotate periodically rotate the IAM credentials
elbayaaa Apr 20, 2021
1721c35
typo
elbayaaa Apr 20, 2021
15b64d9
Use AES instead of KMS for S3 SSE
elbayaaa Apr 21, 2021
b1e31c0
rename Ecr to ECR and Iam to IAM
elbayaaa Apr 23, 2021
050827a
Grant lambda service explicit permissions to thhe ECR instead of rely…
elbayaaa Apr 23, 2021
0444e0c
Merge branch 'sam-pipelines' into sam-pipelines
elbayaaa Apr 24, 2021
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
2 changes: 1 addition & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ ignore_missing_imports=True
ignore_missing_imports=True

# progressive add typechecks and these modules already complete the process, let's keep them clean
[mypy-samcli.commands.build,samcli.lib.build.*,samcli.commands.local.cli_common.invoke_context,samcli.commands.local.lib.local_lambda,samcli.lib.providers.*]
[mypy-samcli.commands.build,samcli.lib.build.*,samcli.commands.local.cli_common.invoke_context,samcli.commands.local.lib.local_lambda,samcli.lib.providers.*,samcli.lib.cookiecutter.*,samcli.lib.pipeline.*,samcli.commands.pipeline.*]
disallow_untyped_defs=True
disallow_incomplete_defs=True
1 change: 1 addition & 0 deletions samcli/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"samcli.commands.deploy",
"samcli.commands.logs",
"samcli.commands.publish",
"samcli.commands.pipeline.pipeline",
# We intentionally do not expose the `bootstrap` command for now. We might open it up later
# "samcli.commands.bootstrap",
]
Expand Down
6 changes: 3 additions & 3 deletions samcli/commands/_utils/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
import yaml
from botocore.utils import set_value_from_jmespath

from samcli.commands.exceptions import UserException
from samcli.lib.utils.packagetype import ZIP
from samcli.yamlhelper import yaml_parse, yaml_dump
from samcli.commands._utils.resources import (
METADATA_WITH_LOCAL_PATHS,
RESOURCES_WITH_LOCAL_PATHS,
AWS_SERVERLESS_FUNCTION,
AWS_LAMBDA_FUNCTION,
get_packageable_resource_paths,
)
from samcli.commands.exceptions import UserException
from samcli.lib.utils.packagetype import ZIP
from samcli.yamlhelper import yaml_parse, yaml_dump


class TemplateNotFoundException(UserException):
Expand Down
Empty file.
Empty file.
221 changes: 221 additions & 0 deletions samcli/commands/pipeline/bootstrap/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
"""
CLI command for "pipeline bootstrap", which sets up the require pipeline infrastructure resources
"""
import os
from typing import Any, cast, Dict, List, Optional

import click

from samcli.cli.cli_config_file import configuration_option, TomlProvider
from samcli.cli.context import get_cmd_names
from samcli.cli.main import pass_context, common_options, aws_creds_options, print_cmdline_args
from samcli.lib.config.samconfig import SamConfig
from samcli.lib.pipeline.bootstrap.stage import Stage
from samcli.lib.telemetry.metric import track_command
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 user(IAM User) with AccessKeyId and SecretAccessKey credentials to be shared with the CI/CD provider
\n\t - Pipeline Execution Role(IAM Role) that is assumed by the Pipeline user to obtain access to the AWS account
\n\t - CloudFormation Execution Role(IAM Role) that is assumed by CloudFormation to deploy the SAM application
\n\t - Artifacts bucket(S3 bucket) to store the sam build artifacts
\n\t - ECR repo for the container images of Lambda functions having PackageType property set to Image
"""

PIPELINE_CONFIG_DIR = os.path.join(".aws-sam", "pipeline")
PIPELINE_CONFIG_FILENAME = "pipelineconfig.toml"


@click.command("bootstrap", short_help=SHORT_HELP, help=HELP_TEXT, context_settings=dict(max_content_width=120))
@configuration_option(provider=TomlProvider(section="parameters"))
@click.option(
"--interactive/--no-interactive",
is_flag=True,
default=True,
help="Disable interactive prompting for bootstrap parameters, and fail if any required arguments are missing.",
)
@click.option(
"--stage-name",
help="The name of the corresponding pipeline stage. It is used as a suffix for the created resources.",
required=False,
)
@click.option(
"--pipeline-user",
help="The ARN of the IAM user having its AccessKeyId and SecretAccessKey shared with the CI/CD provider."
Copy link
Contributor

Choose a reason for hiding this comment

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

same as above, AccessKeyId and SecretAccessKey

"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 AccessKeyId and SecretAccessKey credentials.",
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 stage. "
Copy link
Contributor

Choose a reason for hiding this comment

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

"pipeline-user" -> "pipeline user"

"Provide it only if you want to user your own role, otherwise, the command will create one",
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 "
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
help="The ARN of an IAM Role to be assumed by the CloudFormation service while deploying the application's stack "
help="The ARN of an IAM Role to be assumed by the CloudFormation service while deploying the application's stack. "

image

"Provide it only if you want to user your own role, otherwise, the command will create one",
required=False,
)
@click.option(
"--artifacts-bucket",
help="The ARN of a S3 bucket to hold the sam build artifacts. "
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
help="The ARN of a S3 bucket to hold the sam build artifacts. "
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",
required=False,
)
@click.option(
"--create-ecr-repo/--no-create-ecr-repo",
is_flag=True,
default=False,
help="If set to true and no ecr-repo is provided this command will create an ECR repo to hold the image container "
"of the lambda functions having Image package type.",
)
@click.option(
"--ecr-repo",
help="The ARN of an ECR repo to hold the image containers of the lambda functions of image package type. "
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
help="The ARN of an ECR repo to hold the image containers of the lambda functions of image package type. "
help="The ARN of an ECR repository to store the image containers of the lambda functions of image package type. "

Copy link
Contributor

Choose a reason for hiding this comment

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

Above we wrote:

ECR repo for the container images of Lambda functions having PackageType property set to Image

I think we need to find a consistent way to describe PackageType=Image

"If Provided, the create-ecr-repo argument is ignored. If not provided and create-ecr-repo is set to true "
"The command will create one.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"If Provided, the create-ecr-repo argument is ignored. If not provided and create-ecr-repo is set to true "
"The command will create one.",
"If Provided, the create-ecr-repo argument is ignored. If not provided and create-ecr-repo is set to true, "
"the command will create one.",

required=False,
)
@click.option(
"--pipeline-ip-range",
help="If provided, all requests coming from outside of the given range(s) are denied.",
Copy link
Contributor

Choose a reason for hiding this comment

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

It is not apparent what format customers should use here. Maybe provide some examples

required=False,
)
@click.option(
"--confirm-changeset/--no-confirm-changeset",
default=True,
is_flag=True,
help="Prompt to confirm if the resources is to be deployed by SAM CLI.",
)
@common_options
@aws_creds_options
@pass_context
@track_command
@check_newer_version
@print_cmdline_args
def cli(
ctx: Any,
interactive: bool,
stage_name: Optional[str],
pipeline_user: Optional[str],
pipeline_execution_role: Optional[str],
cloudformation_execution_role: Optional[str],
artifacts_bucket: Optional[str],
create_ecr_repo: bool,
ecr_repo: Optional[str],
pipeline_ip_range: Optional[str],
confirm_changeset: bool,
config_file: Optional[str],
config_env: Optional[str],
) -> None:
"""
`sam pipeline bootstrap` command entry point
"""
do_cli(
region=ctx.region,
profile=ctx.profile,
interactive=interactive,
stage_name=stage_name,
pipeline_user_arn=pipeline_user,
pipeline_execution_role_arn=pipeline_execution_role,
cloudformation_execution_role_arn=cloudformation_execution_role,
artifacts_bucket_arn=artifacts_bucket,
create_ecr_repo=create_ecr_repo,
ecr_repo_arn=ecr_repo,
pipeline_ip_range=pipeline_ip_range,
confirm_changeset=confirm_changeset,
config_file=config_env,
config_env=config_file,
) # pragma: no cover


def do_cli(
region: Optional[str],
profile: Optional[str],
interactive: bool,
stage_name: Optional[str],
pipeline_user_arn: Optional[str],
pipeline_execution_role_arn: Optional[str],
cloudformation_execution_role_arn: Optional[str],
artifacts_bucket_arn: Optional[str],
create_ecr_repo: bool,
ecr_repo_arn: Optional[str],
pipeline_ip_range: Optional[str],
confirm_changeset: bool,
config_file: Optional[str],
config_env: Optional[str],
) -> None:
"""
implementation of `sam pipeline bootstrap` command
"""
if not pipeline_user_arn:
pipeline_user_arn = _load_saved_pipeline_user()

if interactive:
guided_context = GuidedContext(
stage_name=stage_name,
pipeline_user_arn=pipeline_user_arn,
pipeline_execution_role_arn=pipeline_execution_role_arn,
cloudformation_execution_role_arn=cloudformation_execution_role_arn,
artifacts_bucket_arn=artifacts_bucket_arn,
create_ecr_repo=create_ecr_repo,
ecr_repo_arn=ecr_repo_arn,
pipeline_ip_range=pipeline_ip_range,
)
guided_context.run()
stage_name = guided_context.stage_name
pipeline_user_arn = guided_context.pipeline_user_arn
pipeline_execution_role_arn = guided_context.pipeline_execution_role_arn
pipeline_ip_range = guided_context.pipeline_ip_range
cloudformation_execution_role_arn = guided_context.cloudformation_execution_role_arn
artifacts_bucket_arn = guided_context.artifacts_bucket_arn
create_ecr_repo = guided_context.create_ecr_repo
ecr_repo_arn = guided_context.ecr_repo_arn

if not stage_name:
raise click.UsageError("Missing required parameter '--stage-name'")

stage: Stage = Stage(
name=stage_name,
aws_profile=profile,
aws_region=region,
pipeline_user_arn=pipeline_user_arn,
pipeline_execution_role_arn=pipeline_execution_role_arn,
pipeline_ip_range=pipeline_ip_range,
cloudformation_execution_role_arn=cloudformation_execution_role_arn,
artifacts_bucket_arn=artifacts_bucket_arn,
create_ecr_repo=create_ecr_repo,
ecr_repo_arn=ecr_repo_arn,
)

stage.bootstrap(confirm_changeset=confirm_changeset)

stage.print_resources_summary()

try:
stage.save_config(
config_dir=PIPELINE_CONFIG_DIR, filename=PIPELINE_CONFIG_FILENAME, cmd_names=_get_command_name()
)
except Exception:
Copy link
Contributor

Choose a reason for hiding this comment

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

What are the possible exception types here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intention is to capture all kind of exceptions(including unexpected ones); At this point the resources are already bootstrapped, we don't prompt/confirm with the users that we are going to save these configs, i.e. they are unaware of that. So, the bootstrap command has already succeeded and the users should face any failure messages.

# Swallow saving exceptions, if any, as the resources are already bootstrapped and the ARNs are already
# printed out in the screen.
pass


def _load_saved_pipeline_user() -> Optional[str]:
samconfig: SamConfig = SamConfig(config_dir=PIPELINE_CONFIG_DIR, filename=PIPELINE_CONFIG_FILENAME)
if not samconfig.exists():
return None
config: Dict[str, str] = samconfig.get_all(cmd_names=_get_command_name(), section="parameters")
return config.get("pipeline_user")


def _get_command_name() -> List[str]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
def _get_command_name() -> List[str]:
def _get_command_name() -> List[str]:
"""
Return the current command names. For example, if the command is `sam pipeline bootstrap`,
it returns ["pipeline", "bootstrap"]
question: will arguments being printed as well?
"""

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why do you want to add docstring to a private method?
The method that do the actual logic, i.e. get_cmd_names is already documented

"""
Given the click core context, return a list representing all the subcommands passed to the CLI
Parameters
----------
cmd_name : name of current command
ctx : click.Context
Returns
-------
list(str)
List containing subcommand names. Ex: ["local", "start-api"]
"""

Copy link
Contributor

Choose a reason for hiding this comment

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

why do you want to add docstring to a private method?

Because it is not intuitively clear what this function will output. If I just look at it, it involves click and another function. Without the click context, docstring can help to understand what it does.

Copy link
Contributor

Choose a reason for hiding this comment

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

Besides, get_cmd_names() is a confusing function itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ummmm, are you saying the function name _get_command_name doesn't clearly indicates that this function returns the name of the command?

this function just combine two lines of code together instead of duplicating them every time.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah.. the confusing part is, which of the following is the output?

  • ["sam", "pipeline", "bootstrap"]
  • ["sam", "pipeline"]
  • ["pipeline", "bootstrap"]
    If I cannot get the answer by looking at the function code, I think it deserves a docstring (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

then this should be fixed in the public method get_cmd_names() itself, right?

ctx = click.get_current_context()
cmd_names: List[str] = cast(List[str], get_cmd_names(ctx.info_name, ctx)) # ["pipeline", "bootstrap"]
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of casts would be better to update the function type hints.

return cmd_names
94 changes: 94 additions & 0 deletions samcli/commands/pipeline/bootstrap/guided_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
An interactive flow that prompt the user for required information to bootstrap the AWS account of a pipeline stage
with the required infrastructure
"""
from typing import Optional

import click


class GuidedContext:
def __init__(
self,
stage_name: Optional[str] = None,
pipeline_user_arn: Optional[str] = None,
pipeline_execution_role_arn: Optional[str] = None,
cloudformation_execution_role_arn: Optional[str] = None,
artifacts_bucket_arn: Optional[str] = None,
create_ecr_repo: bool = False,
ecr_repo_arn: Optional[str] = None,
pipeline_ip_range: Optional[str] = None,
) -> None:
self.stage_name = stage_name
self.pipeline_user_arn = pipeline_user_arn
self.pipeline_execution_role_arn = pipeline_execution_role_arn
self.cloudformation_execution_role_arn = cloudformation_execution_role_arn
self.artifacts_bucket_arn = artifacts_bucket_arn
self.create_ecr_repo = create_ecr_repo
self.ecr_repo_arn = ecr_repo_arn
self.pipeline_ip_range = pipeline_ip_range

def run(self) -> None:
"""
Runs an interactive questionnaire to prompt the user for the ARNs of the AWS resources(infrastructure) required
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
"""
if not self.stage_name:
self.stage_name = click.prompt("Stage Name", type=click.STRING)

if not self.pipeline_user_arn:
click.echo(
"\nThere must be exactly one pipeline user across all of the pipeline stages. "
"If you have ran this command before to bootstrap a previous pipeline stage, please "
"provide the ARN of the created pipeline user, otherwise, we will create a new user for you, "
"please make sure to configure user's AccessKeyId and SecretAccessKey for the CI/CD provider."
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"provide the ARN of the created pipeline user, otherwise, we will create a new user for you, "
"please make sure to configure user's AccessKeyId and SecretAccessKey for the CI/CD provider."
"provide the ARN of the created pipeline user, otherwise, we will create a new user for you. "
"Please make sure to store the credentials safely with the CI/CD provider."

)
self.pipeline_user_arn = click.prompt(
"Pipeline user [leave blank to create one]", default="", type=click.STRING
)

if not self.pipeline_execution_role_arn:
self.pipeline_execution_role_arn = click.prompt(
"\nPipeline execution role(an IAM Role to be assumed by the pipeline-user to operate on this stage.) "
"[leave blank to create one]",
default="",
type=click.STRING,
)

if not self.cloudformation_execution_role_arn:
self.cloudformation_execution_role_arn = click.prompt(
"\nCloudFormation execution role(an IAM Role to be assumed by the CloudFormation service to deploy "
"the application's stack) [leave blank to create one]",
default="",
type=click.STRING,
)

if not self.artifacts_bucket_arn:
self.artifacts_bucket_arn = click.prompt(
"\nArtifacts bucket(S3 bucket to hold the sam build artifacts) " "[leave blank to create one]",
default="",
type=click.STRING,
)
if not self.ecr_repo_arn:
click.echo(
"\nIf your SAM template will include lambda functions of Image package-type, "
"then an ECR repo is required, should we create one?"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"\nIf your SAM template will include lambda functions of Image package-type, "
"then an ECR repo is required, should we create one?"
"\nIf your SAM template will include Lambda functions of Image package type, "
"then an ECR repository is required. Should we create one?"

Copy link
Contributor

Choose a reason for hiding this comment

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

besides, we should use if ... template includes instead of will include here. See https://writingcenter.unc.edu/tips-and-tools/conditionals-verb-tense-in-if-clauses/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I used the future tense to let the users think about the future; Instead of thinking "no my templates doesn't contain this" they will think "will my template contain this".

Copy link
Contributor

Choose a reason for hiding this comment

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

but it is not how "if" is used in English as far as I know. You can check the article I posted above, they cover all scenarios and none of them use future tense. (specifically about the "likely")

Copy link
Contributor

Choose a reason for hiding this comment

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

)
click.echo("\t1 - No, My SAM Template won't include lambda functions of Image package-type")
click.echo("\t2 - Yes, I need a help creating one")
click.echo("\t3 - I already have an ECR repo")
choice = click.prompt(text="Choice", show_choices=False, type=click.Choice(["1", "2", "3"]))
if choice == "1":
self.create_ecr_repo = False
elif choice == "2":
self.create_ecr_repo = True
else: # choice == "3"
self.create_ecr_repo = False
self.ecr_repo_arn = click.prompt("ECR repo", type=click.STRING)

if not self.pipeline_ip_range:
click.echo("\nWe can deny requests if not coming from a recognized IP address.")
self.pipeline_ip_range = click.prompt(
"Pipeline IP address range [leave blank if you don't know]", default="", type=click.STRING
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"Pipeline IP address range [leave blank if you don't know]", default="", type=click.STRING
"Pipeline IP address range (using CIDR notation) [leave blank if you don't know]", default="", type=click.STRING

)
19 changes: 19 additions & 0 deletions samcli/commands/pipeline/pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Command group for "pipeline" suite for commands. It provides common CLI arguments, template parsing capabilities,
setting up stdin/stdout etc
"""

import click

from .bootstrap.cli import cli as bootstrap_cli


@click.group()
def cli() -> None:
"""
Manage the continuous delivery of the application
"""


# Add individual commands under this group
cli.add_command(bootstrap_cli)
Loading