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 all 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.*,samcli.lib.utils.git_repo.py]
[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.utils.git_repo.py,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
4 changes: 2 additions & 2 deletions samcli/cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import logging
import uuid
from typing import Optional, cast
from typing import Optional, cast, List

import boto3
import botocore
Expand Down Expand Up @@ -186,7 +186,7 @@ def _refresh_session(self):
raise CredentialsError(str(ex)) from ex


def get_cmd_names(cmd_name, ctx):
def get_cmd_names(cmd_name, ctx) -> List[str]:
"""
Given the click core context, return a list representing all the subcommands passed to the CLI

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.
216 changes: 216 additions & 0 deletions samcli/commands/pipeline/bootstrap/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
"""
CLI command for "pipeline bootstrap", which sets up the require pipeline infrastructure resources
"""
import os
from typing import Any, 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 IAM user with access key ID and secret access key credentials to be shared with the CI/CD provider
\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 repository to hold container image Lambda deployment packages
"""

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 access key ID and secret access key shared with the CI/CD provider. "
"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.",
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. "
"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. "
"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 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 repository is provided, this command will create an ECR repository to hold the"
" container images of Lambda functions having an Image package type.",
)
@click.option(
"--ecr-repo",
help="The ARN of an ECR repository to hold the containers images of Lambda functions of Image package type. "
"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 are denied. Example: 10.24.34.0/24",
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_arn()

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,
)

bootstrapped: bool = stage.bootstrap(confirm_changeset=confirm_changeset)

if bootstrapped:
stage.print_resources_summary()

stage.save_config_safe(
config_dir=PIPELINE_CONFIG_DIR, filename=PIPELINE_CONFIG_FILENAME, cmd_names=_get_command_names()
)


def _load_saved_pipeline_user_arn() -> 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_names(), section="parameters")
return config.get("pipeline_user")


def _get_command_names() -> List[str]:
ctx = click.get_current_context()
return get_cmd_names(ctx.info_name, ctx) # ["pipeline", "bootstrap"]
96 changes: 96 additions & 0 deletions samcli/commands/pipeline/bootstrap/guided_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
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 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 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 assumed by CloudFormation 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 AWS SAM build artifacts) [leave blank to create one]",
default="",
type=click.STRING,
)
if not self.ecr_repo_arn:
click.echo(
"\nIf your SAM template includes (or going to include) Lambda functions of Image package type, "
"then an ECR repository is required. Should we create one?"
)
click.echo("\t1 - No, My SAM template won't include Lambda functions of Image package type")
click.echo("\t2 - Yes, I need help creating one")
click.echo("\t3 - I already have an ECR repository")
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 not coming from a recognized IP address range.")
self.pipeline_ip_range = click.prompt(
"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