diff --git a/samcli/commands/pipeline/bootstrap/cli.py b/samcli/commands/pipeline/bootstrap/cli.py index aaeef3cfda..b8f1996683 100644 --- a/samcli/commands/pipeline/bootstrap/cli.py +++ b/samcli/commands/pipeline/bootstrap/cli.py @@ -2,6 +2,7 @@ CLI command for "pipeline bootstrap", which sets up the require pipeline infrastructure resources """ import os +from textwrap import dedent from typing import Any, Dict, List, Optional import click @@ -206,14 +207,26 @@ def do_cli( ) click.secho( - Colored().green( - "\nThe ARNs of created resources have been written to " - f"{os.path.join(PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME)}.\n" - f"It will be used next time you run `sam pipeline bootstrap` or " - f"`sam pipeline init` in this directory." + 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. + """ ) ) + if not environment.pipeline_user.is_user_provided: + click.secho( + dedent( + f"""\ + Before running {Colored().bold("sam pipeline init")}, we recommend first setting up AWS credentials + in your CI/CD account. Read more about how to do so with your provider in + [DOCS-LINK]. + """ + ) + ) + def _load_saved_pipeline_user_arn() -> Optional[str]: samconfig: SamConfig = SamConfig(config_dir=PIPELINE_CONFIG_DIR, filename=PIPELINE_CONFIG_FILENAME) diff --git a/samcli/commands/pipeline/bootstrap/guided_context.py b/samcli/commands/pipeline/bootstrap/guided_context.py index 982203285e..d7f21044b2 100644 --- a/samcli/commands/pipeline/bootstrap/guided_context.py +++ b/samcli/commands/pipeline/bootstrap/guided_context.py @@ -2,10 +2,15 @@ An interactive flow that prompt the user for required information to bootstrap the AWS account of an environment with the required infrastructure """ -from typing import Optional +import sys +from textwrap import dedent +from typing import Optional, List, Tuple, Callable import click + +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 @@ -32,82 +37,198 @@ def __init__( self.image_repository_arn = image_repository_arn self.pipeline_ip_range = pipeline_ip_range self.region = region + self.color = Colored() + + 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." + ) + self.environment_name = click.prompt( + "Stage name", + default=self.environment_name, + type=click.STRING, + ) + + def _prompt_region_name(self) -> None: + self.region = click.prompt( + "Enter the region you want these resources to create", + type=click.STRING, + default=get_default_aws_region(), + ) + + def _prompt_pipeline_user(self) -> None: + self.pipeline_user_arn = click.prompt( + "Enter the pipeline IAM user ARN if you have previously created one, or we will create one for you", + default="", + type=click.STRING, + ) + + def _prompt_pipeline_execution_role(self) -> None: + self.pipeline_execution_role_arn = click.prompt( + "Enter the pipeline execution role ARN if you have previously created one, " + "or we will create one for you", + default="", + type=click.STRING, + ) + + def _prompt_cloudformation_execution_role(self) -> None: + self.cloudformation_execution_role_arn = click.prompt( + "Enter the CloudFormation execution role ARN if you have previously created one, " + "or we will create one for you", + default="", + type=click.STRING, + ) + + def _prompt_artifacts_bucket(self) -> None: + self.artifacts_bucket_arn = click.prompt( + "Please enter the artifact bucket ARN for your Lambda function. " + "If you do not have a bucket, we will create one for you", + default="", + type=click.STRING, + ) + + 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", + default="", + type=click.STRING, + ) + self.create_image_repository = not bool(self.image_repository_arn) + else: + self.create_image_repository = False + + def _prompt_ip_range(self) -> None: + self.pipeline_ip_range = click.prompt( + "For added security, you can define the permitted pipeline IP range. " + "Enter the IP addresses to restrict access to", + default="", + type=click.STRING, + ) - def run(self) -> None: + def _get_user_inputs(self) -> List[Tuple[str, Callable[[], None]]]: + return [ + (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", + 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", + 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", + self._prompt_cloudformation_execution_role, + ), + ( + f"Artifacts bucket ARN: {self.artifacts_bucket_arn}" + if self.artifacts_bucket_arn + 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'}", + self._prompt_image_repository, + ), + ( + f"Pipeline IP address range: {self.pipeline_ip_range}" + if self.pipeline_ip_range + else "Pipeline IP address range: none", + self._prompt_ip_range, + ), + ] + + def run(self) -> None: # pylint: disable=too-many-branches """ 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 """ + 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() - if not self.environment_name: - self.environment_name = click.prompt( - f"Environment name (a descriptive name for the environment which will be deployed" - f" to AWS account {account_id})", - type=click.STRING, - ) + 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) + if self.environment_name: + click.echo(f"Stage name: {self.environment_name}") + else: + self._prompt_stage_name() if not self.region: - self.region = click.prompt( - "\nAWS region (the AWS region where the environment infrastructure resources will be deployed to)", - type=click.STRING, - default=get_default_aws_region(), - ) + self._prompt_region_name() - if not self.pipeline_user_arn: - click.echo( - "\nThere must be exactly one pipeline user across all of the environments. " - "If you have ran this command before to bootstrap a previous environment, 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 system." - ) - self.pipeline_user_arn = click.prompt( - "Pipeline user [leave blank to create one]", default="", type=click.STRING - ) + 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() - 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 environment) " - "[leave blank to create one]", - default="", - type=click.STRING, - ) + if self.pipeline_execution_role_arn: + click.echo(f"Pipeline execution role ARN: {self.pipeline_execution_role_arn}") + else: + self._prompt_pipeline_execution_role() - 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 self.cloudformation_execution_role_arn: + click.echo(f"CloudFormation execution role ARN: {self.cloudformation_execution_role_arn}") + else: + self._prompt_cloudformation_execution_role() - 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.image_repository_arn: - click.echo( - "\nIf your SAM template includes (or going to include) Lambda functions of Image package type, " - "then an ECR image 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 image repository") - choice = click.prompt(text="Choice", show_choices=False, type=click.Choice(["1", "2", "3"])) - if choice == "1": - self.create_image_repository = False - elif choice == "2": - self.create_image_repository = True - else: # choice == "3" - self.create_image_repository = False - self.image_repository_arn = click.prompt("ECR image repository", 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, + if self.artifacts_bucket_arn: + click.echo(f"Artifacts bucket ARN: {self.cloudformation_execution_role_arn}") + else: + self._prompt_artifacts_bucket() + + if self.image_repository_arn: + click.echo(f"ECR image repository ARN: {self.image_repository_arn}") + else: + self._prompt_image_repository() + + click.secho("[4] Security definition - OPTIONAL", bold=True) + if self.pipeline_ip_range: + click.echo(f"Pipeline IP address range: {self.pipeline_ip_range}") + else: + self._prompt_ip_range() + + # Ask customers to confirm the inputs + while True: + inputs = self._get_user_inputs() + click.secho(self.color.cyan("Below is the summary of the answers:")) + for i, (text, _) in enumerate(inputs): + click.secho(self.color.cyan(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", + show_choices=False, + show_default=False, + type=click.Choice(["0"] + [str(i + 1) for i in range(len(inputs))]), ) + if int(edit_input): + inputs[int(edit_input) - 1][1]() + else: + break diff --git a/samcli/commands/pipeline/external_links.py b/samcli/commands/pipeline/external_links.py new file mode 100644 index 0000000000..cb70320c00 --- /dev/null +++ b/samcli/commands/pipeline/external_links.py @@ -0,0 +1,5 @@ +""" +The module to store external links. Put them in a centralized place so that we can verify their +validity automatically. +""" +CONFIG_AWS_CRED_DOC_URL = "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html" diff --git a/samcli/lib/pipeline/bootstrap/environment.py b/samcli/lib/pipeline/bootstrap/environment.py index afad534c65..260842e7c9 100644 --- a/samcli/lib/pipeline/bootstrap/environment.py +++ b/samcli/lib/pipeline/bootstrap/environment.py @@ -3,6 +3,7 @@ import os import pathlib import re +from itertools import chain from typing import Dict, List, Optional, Tuple import boto3 @@ -91,13 +92,19 @@ def __init__( self.name: str = name self.aws_profile: Optional[str] = aws_profile self.aws_region: Optional[str] = aws_region - self.pipeline_user: IAMUser = IAMUser(arn=pipeline_user_arn) - self.pipeline_execution_role: Resource = Resource(arn=pipeline_execution_role_arn) + self.pipeline_user: IAMUser = IAMUser(arn=pipeline_user_arn, comment="Pipeline IAM user") + self.pipeline_execution_role: Resource = Resource( + arn=pipeline_execution_role_arn, comment="Pipeline execution role" + ) self.pipeline_ip_range: Optional[str] = pipeline_ip_range - self.cloudformation_execution_role: Resource = Resource(arn=cloudformation_execution_role_arn) - self.artifacts_bucket: Resource = Resource(arn=artifacts_bucket_arn) + self.cloudformation_execution_role: Resource = Resource( + arn=cloudformation_execution_role_arn, comment="CloudFormation execution role" + ) + self.artifacts_bucket: Resource = Resource(arn=artifacts_bucket_arn, comment="Artifact bucket") self.create_image_repository: bool = create_image_repository - self.image_repository: ECRImageRepository = ECRImageRepository(arn=image_repository_arn) + self.image_repository: ECRImageRepository = ECRImageRepository( + arn=image_repository_arn, comment="ECR image repository" + ) self.color = Colored() def did_user_provide_all_required_resources(self) -> bool: @@ -105,18 +112,20 @@ def did_user_provide_all_required_resources(self) -> bool: return all(resource.is_user_provided for resource in self._get_resources()) def _get_non_user_provided_resources_msg(self) -> str: - missing_resources_msg = "" - if not self.pipeline_user.is_user_provided: - missing_resources_msg += "\n\tPipeline user" - if not self.pipeline_execution_role.is_user_provided: - missing_resources_msg += "\n\tPipeline execution role." - if not self.cloudformation_execution_role.is_user_provided: - missing_resources_msg += "\n\tCloudFormation execution role." - if not self.artifacts_bucket.is_user_provided: - missing_resources_msg += "\n\tArtifacts bucket." - if self.create_image_repository and not self.image_repository.is_user_provided: - missing_resources_msg += "\n\tECR image repository." - return missing_resources_msg + resource_comments = chain.from_iterable( + [ + [] if self.pipeline_user.is_user_provided else [self.pipeline_user.comment], + [] if self.pipeline_execution_role.is_user_provided else [self.pipeline_execution_role.comment], + [] + if self.cloudformation_execution_role.is_user_provided + else [self.cloudformation_execution_role.comment], + [] if self.artifacts_bucket.is_user_provided else [self.artifacts_bucket.comment], + [] + if self.image_repository.is_user_provided or not self.create_image_repository + else [self.image_repository.comment], + ] + ) + return "\n".join([f" - {comment}" for comment in resource_comments]) def bootstrap(self, confirm_changeset: bool = True) -> bool: """ @@ -149,7 +158,7 @@ def bootstrap(self, confirm_changeset: bool = True) -> bool: missing_resources_msg: str = self._get_non_user_provided_resources_msg() click.echo( - f"This will create the following required resources for the '{self.name}' environment: " + f"This will create the following required resources for the '{self.name}' environment: \n" f"{missing_resources_msg}" ) if confirm_changeset: @@ -307,28 +316,12 @@ def print_resources_summary(self) -> None: created_resources.append(resource) if created_resources: - click.secho(self.color.green("\nWe have created the following resources:")) + click.secho(self.color.green("The following resources were created in your account:")) for resource in created_resources: - click.secho(f"\t{resource.arn}", fg="green") - - if provided_resources: - click.secho( - self.color.green( - "\nYou provided the following resources. Please make sure it has the required permissions " - "as shown at https://github.com/aws/aws-sam-cli/blob/develop/" - "samcli/lib/pipeline/bootstrap/environment_resources.yaml", - ) - ) - for resource in provided_resources: - click.secho(self.color.green(f"\t{resource.arn}")) + click.secho(self.color.green(f" - {resource.comment}")) if not self.pipeline_user.is_user_provided: - click.secho( - self.color.green( - "Please configure your CI/CD project with the following pipeline user credentials and " - "make sure to periodically rotate it:", - ) - ) + click.secho(self.color.green("Pipeline IAM user credential:")) click.secho(self.color.green(f"\tAWS_ACCESS_KEY_ID: {self.pipeline_user.access_key_id}")) click.secho(self.color.green(f"\tAWS_SECRET_ACCESS_KEY: {self.pipeline_user.secret_access_key}")) diff --git a/samcli/lib/pipeline/bootstrap/resource.py b/samcli/lib/pipeline/bootstrap/resource.py index a2bd3cb284..a7b39dd965 100644 --- a/samcli/lib/pipeline/bootstrap/resource.py +++ b/samcli/lib/pipeline/bootstrap/resource.py @@ -44,6 +44,8 @@ class Resource: ---------- arn: str the ARN of the resource + comment: str + the comment of the resource is_user_provided: bool True if the user provided the ARN of the resource during the initialization. It indicates whether this pipeline- resource is provided by the user or created by SAM during `sam pipeline bootstrap` @@ -54,8 +56,9 @@ class Resource: extracts and returns the resource name from its ARN """ - def __init__(self, arn: Optional[str]) -> None: + def __init__(self, arn: Optional[str], comment: Optional[str]) -> None: self.arn: Optional[str] = arn + self.comment: Optional[str] = comment self.is_user_provided: bool = bool(arn) def name(self) -> Optional[str]: @@ -83,11 +86,15 @@ class IAMUser(Resource): """ def __init__( - self, arn: Optional[str], access_key_id: Optional[str] = None, secret_access_key: Optional[str] = None + self, + arn: Optional[str], + comment: Optional[str], + access_key_id: Optional[str] = None, + secret_access_key: Optional[str] = None, ) -> None: self.access_key_id: Optional[str] = access_key_id self.secret_access_key: Optional[str] = secret_access_key - super().__init__(arn=arn) + super().__init__(arn=arn, comment=comment) class S3Bucket(Resource): @@ -99,16 +106,16 @@ class S3Bucket(Resource): The ARN of the KMS key used in encrypting this S3Bucket, if any. """ - def __init__(self, arn: Optional[str], kms_key_arn: Optional[str] = None) -> None: + def __init__(self, arn: Optional[str], comment: Optional[str], kms_key_arn: Optional[str] = None) -> None: self.kms_key_arn: Optional[str] = kms_key_arn - super().__init__(arn=arn) + super().__init__(arn=arn, comment=comment) class ECRImageRepository(Resource): """ Represents an AWS ECR image repository resource """ - def __init__(self, arn: Optional[str]) -> None: - super().__init__(arn=arn) + def __init__(self, arn: Optional[str], comment: Optional[str]) -> None: + super().__init__(arn=arn, comment=comment) def get_uri(self) -> Optional[str]: """ diff --git a/samcli/lib/utils/colors.py b/samcli/lib/utils/colors.py index 84e3cbdbd7..84767f0fec 100644 --- a/samcli/lib/utils/colors.py +++ b/samcli/lib/utils/colors.py @@ -58,6 +58,10 @@ def underline(self, msg): """Underline the input""" return click.style(msg, underline=True) if self.colorize else msg + def bold(self, msg): + """Bold the input""" + return click.style(msg, bold=True) if self.colorize else msg + def _color(self, msg, color): """Internal helper method to add colors to input""" kwargs = {"fg": color} diff --git a/tests/unit/commands/pipeline/bootstrap/test_guided_context.py b/tests/unit/commands/pipeline/bootstrap/test_guided_context.py index 65db1edd96..2d40e5454c 100644 --- a/tests/unit/commands/pipeline/bootstrap/test_guided_context.py +++ b/tests/unit/commands/pipeline/bootstrap/test_guided_context.py @@ -1,5 +1,5 @@ from unittest import TestCase -from unittest.mock import patch +from unittest.mock import patch, Mock from samcli.commands.pipeline.bootstrap.guided_context import GuidedContext @@ -19,6 +19,8 @@ class TestGuidedContext(TestCase): @patch("samcli.commands.pipeline.bootstrap.guided_context.click") def test_guided_context_will_not_prompt_for_fields_that_are_already_provided(self, click_mock, account_id_mock): account_id_mock.return_value = "1234567890" + click_mock.confirm.return_value = False + click_mock.prompt = Mock(return_value="0") gc: GuidedContext = GuidedContext( environment_name=ANY_ENVIRONMENT_NAME, pipeline_user_arn=ANY_PIPELINE_USER_ARN, @@ -31,23 +33,26 @@ def test_guided_context_will_not_prompt_for_fields_that_are_already_provided(sel region=ANY_REGION, ) gc.run() - click_mock.prompt.assert_not_called() + # there should only one prompt to ask what values customers want to change + click_mock.prompt.assert_called_once() @patch("samcli.commands.pipeline.bootstrap.guided_context.get_current_account_id") @patch("samcli.commands.pipeline.bootstrap.guided_context.click") def test_guided_context_will_prompt_for_fields_that_are_not_provided(self, click_mock, account_id_mock): account_id_mock.return_value = "1234567890" + click_mock.confirm.return_value = False + click_mock.prompt = Mock(return_value="0") gc: GuidedContext = GuidedContext( image_repository_arn=ANY_IMAGE_REPOSITORY_ARN # Exclude ECR repo, it has its own detailed test below ) gc.run() - self.assertTrue(self.did_prompt_text_like("Environment Name", click_mock.prompt)) - self.assertTrue(self.did_prompt_text_like("Pipeline user", click_mock.prompt)) + self.assertTrue(self.did_prompt_text_like("Stage Name", click_mock.prompt)) + self.assertTrue(self.did_prompt_text_like("Pipeline IAM user", click_mock.prompt)) self.assertTrue(self.did_prompt_text_like("Pipeline execution role", click_mock.prompt)) self.assertTrue(self.did_prompt_text_like("CloudFormation execution role", click_mock.prompt)) - self.assertTrue(self.did_prompt_text_like("Artifacts bucket", click_mock.prompt)) - self.assertTrue(self.did_prompt_text_like("AWS region", click_mock.prompt)) - self.assertTrue(self.did_prompt_text_like("Pipeline IP address range", click_mock.prompt)) + self.assertTrue(self.did_prompt_text_like("Artifact bucket", click_mock.prompt)) + self.assertTrue(self.did_prompt_text_like("region", click_mock.prompt)) + self.assertTrue(self.did_prompt_text_like("Pipeline IP range", click_mock.prompt)) @patch("samcli.commands.pipeline.bootstrap.guided_context.get_current_account_id") @patch("samcli.commands.pipeline.bootstrap.guided_context.click") @@ -70,22 +75,27 @@ def test_guided_context_will_not_prompt_for_not_provided_image_repository_if_no_ self.assertIsNone(gc_without_ecr_info.image_repository_arn) - click_mock.prompt.return_value = "1" # the user chose to not CREATE an ECR Image repository + click_mock.confirm.side_effect = [False, False] # the user chose to not CREATE an ECR Image repository + click_mock.prompt.return_value = "0" gc_without_ecr_info.run() self.assertIsNone(gc_without_ecr_info.image_repository_arn) self.assertFalse(gc_without_ecr_info.create_image_repository) - self.assertFalse(self.did_prompt_text_like("ECR image repository", click_mock.prompt)) + self.assertFalse(self.did_prompt_text_like("Please enter the ECR image repository", click_mock.prompt)) - click_mock.prompt.return_value = "2" # the user chose to CREATE an ECR Image repository + click_mock.confirm.side_effect = [False, True] # the user chose to CREATE an ECR Image repository + click_mock.prompt.side_effect = [None, "0"] gc_without_ecr_info.run() self.assertIsNone(gc_without_ecr_info.image_repository_arn) self.assertTrue(gc_without_ecr_info.create_image_repository) - self.assertFalse(self.did_prompt_text_like("ECR image repository", click_mock.prompt)) + self.assertTrue(self.did_prompt_text_like("Please enter the ECR image repository", click_mock.prompt)) - click_mock.prompt.side_effect = ["3", ANY_IMAGE_REPOSITORY_ARN] # the user already has a repo + click_mock.confirm.side_effect = [False, True] # the user already has a repo + click_mock.prompt.side_effect = [ANY_IMAGE_REPOSITORY_ARN, "0"] gc_without_ecr_info.run() self.assertFalse(gc_without_ecr_info.create_image_repository) - self.assertTrue(self.did_prompt_text_like("ECR image repository", click_mock.prompt)) # we've asked about it + self.assertTrue( + self.did_prompt_text_like("Please enter the ECR image repository", click_mock.prompt) + ) # we've asked about it self.assertEqual(gc_without_ecr_info.image_repository_arn, ANY_IMAGE_REPOSITORY_ARN) @staticmethod diff --git a/tests/unit/lib/pipeline/bootstrap/test_environment.py b/tests/unit/lib/pipeline/bootstrap/test_environment.py index ca19c1ce52..09df6bdc2e 100644 --- a/tests/unit/lib/pipeline/bootstrap/test_environment.py +++ b/tests/unit/lib/pipeline/bootstrap/test_environment.py @@ -339,8 +339,7 @@ def test_save_config_safe(self, save_config_mock): def test_print_resources_summary_when_no_resources_provided_by_the_user(self, click_mock): environment: Environment = Environment(name=ANY_ENVIRONMENT_NAME) environment.print_resources_summary() - self.assert_summary_has_a_message_like("We have created the following resources", click_mock.secho) - self.assert_summary_does_not_have_a_message_like("You provided the following resources", click_mock.secho) + self.assert_summary_has_a_message_like("The following resources were created in your account", click_mock.secho) @patch("samcli.lib.pipeline.bootstrap.environment.click") def test_print_resources_summary_when_all_resources_are_provided_by_the_user(self, click_mock): @@ -354,8 +353,9 @@ def test_print_resources_summary_when_all_resources_are_provided_by_the_user(sel image_repository_arn=ANY_IMAGE_REPOSITORY_ARN, ) environment.print_resources_summary() - self.assert_summary_does_not_have_a_message_like("We have created the following resources", click_mock.secho) - self.assert_summary_has_a_message_like("You provided the following resources", click_mock.secho) + self.assert_summary_does_not_have_a_message_like( + "The following resources were created in your account", click_mock.secho + ) @patch("samcli.lib.pipeline.bootstrap.environment.click") def test_print_resources_summary_when_some_resources_are_provided_by_the_user(self, click_mock): @@ -367,8 +367,7 @@ def test_print_resources_summary_when_some_resources_are_provided_by_the_user(se image_repository_arn=ANY_IMAGE_REPOSITORY_ARN, ) environment.print_resources_summary() - self.assert_summary_has_a_message_like("We have created the following resources", click_mock.secho) - self.assert_summary_has_a_message_like("You provided the following resources", click_mock.secho) + self.assert_summary_has_a_message_like("The following resources were created in your account", click_mock.secho) @patch("samcli.lib.pipeline.bootstrap.environment.click") def test_print_resources_summary_prints_the_credentials_of_the_pipeline_user_iff_not_provided_by_the_user( diff --git a/tests/unit/lib/pipeline/bootstrap/test_resource.py b/tests/unit/lib/pipeline/bootstrap/test_resource.py index 7e3ef507da..f7dcab50f2 100644 --- a/tests/unit/lib/pipeline/bootstrap/test_resource.py +++ b/tests/unit/lib/pipeline/bootstrap/test_resource.py @@ -23,18 +23,18 @@ def test_arn_parts_of_invalid_arn(self): class TestResource(TestCase): def test_resource(self): - resource = Resource(arn=VALID_ARN) + resource = Resource(arn=VALID_ARN, comment="") self.assertEqual(resource.arn, VALID_ARN) self.assertTrue(resource.is_user_provided) self.assertEqual(resource.name(), "resource-id") - resource = Resource(arn=INVALID_ARN) + resource = Resource(arn=INVALID_ARN, comment="") self.assertEqual(resource.arn, INVALID_ARN) self.assertTrue(resource.is_user_provided) with self.assertRaises(ValueError): resource.name() - resource = Resource(arn=None) + resource = Resource(arn=None, comment="") self.assertIsNone(resource.arn) self.assertFalse(resource.is_user_provided) self.assertIsNone(resource.name()) @@ -42,13 +42,20 @@ def test_resource(self): class TestIAMUser(TestCase): def test_create_iam_user(self): - user: IAMUser = IAMUser(arn=VALID_ARN) + user: IAMUser = IAMUser(arn=VALID_ARN, comment="user") self.assertEquals(user.arn, VALID_ARN) + self.assertEquals(user.comment, "user") self.assertIsNone(user.access_key_id) self.assertIsNone(user.secret_access_key) - user = IAMUser(arn=INVALID_ARN, access_key_id="any_access_key_id", secret_access_key="any_secret_access_key") + user = IAMUser( + arn=INVALID_ARN, + access_key_id="any_access_key_id", + secret_access_key="any_secret_access_key", + comment="user", + ) self.assertEquals(user.arn, INVALID_ARN) + self.assertEquals(user.comment, "user") self.assertEquals(user.access_key_id, "any_access_key_id") self.assertEquals(user.secret_access_key, "any_secret_access_key") @@ -56,11 +63,12 @@ def test_create_iam_user(self): class TestECRImageRepository(TestCase): def test_get_uri_with_valid_ecr_arn(self): valid_ecr_arn = "arn:partition:service:region:account-id:repository/repository-name" - repo: ECRImageRepository = ECRImageRepository(arn=valid_ecr_arn) + repo: ECRImageRepository = ECRImageRepository(arn=valid_ecr_arn, comment="ecr") self.assertEqual(repo.get_uri(), "account-id.dkr.ecr.region.amazonaws.com/repository-name") + self.assertEquals("ecr", repo.comment) def test_get_uri_with_invalid_ecr_arn(self): - repo = ECRImageRepository(arn=INVALID_ARN) + repo = ECRImageRepository(arn=INVALID_ARN, comment="ecr") with self.assertRaises(ValueError): repo.get_uri() @@ -68,6 +76,6 @@ def test_get_uri_with_valid_aws_arn_that_is_invalid_ecr_arn(self): ecr_arn_missing_repository_prefix = ( "arn:partition:service:region:account-id:repository-name-without-repository/-prefix" ) - repo = ECRImageRepository(arn=ecr_arn_missing_repository_prefix) + repo = ECRImageRepository(arn=ecr_arn_missing_repository_prefix, comment="ecr") with self.assertRaises(ValueError): repo.get_uri()