diff --git a/samcli/commands/exceptions.py b/samcli/commands/exceptions.py index d560dd9fad..8fdaaaf438 100644 --- a/samcli/commands/exceptions.py +++ b/samcli/commands/exceptions.py @@ -2,6 +2,8 @@ Class containing error conditions that are exposed to the user. """ +import os + import click @@ -72,3 +74,15 @@ class AppPipelineTemplateManifestException(UserException): Exception class when SAM is not able to parse the "manifest.yaml" file located in the SAM pipeline templates Git repo: "github.com/aws/aws-sam-cli-pipeline-init-templates.git """ + + +class PipelineFileAlreadyExistsError(UserException): + """ + Exception class when the files to-be-generated by the pipeline template already exists on the SAM project. Instead + of overriding, the user need to manually remove the old files. + """ + + def __init__(self, file_path: os.PathLike) -> None: + super().__init__( + f'Pipeline file "{file_path}" already exists in project root directory, please remove it first.' + ) diff --git a/samcli/commands/pipeline/init/interactive_init_flow.py b/samcli/commands/pipeline/init/interactive_init_flow.py index 1388dc387c..4679c6ca82 100644 --- a/samcli/commands/pipeline/init/interactive_init_flow.py +++ b/samcli/commands/pipeline/init/interactive_init_flow.py @@ -10,7 +10,7 @@ import click from samcli.cli.main import global_cfg -from samcli.commands.exceptions import PipelineTemplateCloneException +from samcli.commands.exceptions import PipelineTemplateCloneException, PipelineFileAlreadyExistsError from samcli.lib.config.samconfig import SamConfig from samcli.lib.cookiecutter.interactive_flow import InteractiveFlow from samcli.lib.cookiecutter.interactive_flow_creator import InteractiveFlowCreator @@ -225,6 +225,9 @@ def _prompt_cicd_provider(available_providers: List[Provider]) -> Provider: Returns: The chosen provider """ + if len(available_providers) == 1: + return available_providers[0] + question_to_choose_provider = Choice( key="provider", text="CI/CD provider", @@ -246,6 +249,8 @@ def _prompt_provider_pipeline_template( Returns: The chosen pipeline template manifest """ + if len(provider_available_pipeline_templates_metadata) == 1: + return provider_available_pipeline_templates_metadata[0] question_to_choose_pipeline_template = Choice( key="pipeline-template", text="Which pipeline template would you like to use?", @@ -291,10 +296,3 @@ def _get_pipeline_template_interactive_flow(pipeline_template_dir: Path) -> Inte """ flow_definition_path: Path = pipeline_template_dir.joinpath("questions.json") return InteractiveFlowCreator.create_flow(str(flow_definition_path)) - - -class PipelineFileAlreadyExistsError(Exception): - def __init__(self, file_path: os.PathLike) -> None: - Exception.__init__( - self, f'Pipeline file "{file_path}" already exists in project root directory, please remove it first.' - ) diff --git a/samcli/lib/cookiecutter/question.py b/samcli/lib/cookiecutter/question.py index 8c3fe7ae4f..9cbb67f05d 100644 --- a/samcli/lib/cookiecutter/question.py +++ b/samcli/lib/cookiecutter/question.py @@ -104,6 +104,10 @@ def ask(self, context: Optional[Dict] = None) -> Any: The user provided answer. """ resolved_default_answer = self._resolve_default_answer(context) + # if it is an optional question with no default answer, + # set an empty default answer to prevent click from keep asking for an answer + if not self._required and resolved_default_answer is None: + resolved_default_answer = "" return click.prompt(text=self._text, default=resolved_default_answer) def get_next_question_key(self, answer: Any) -> Optional[str]: diff --git a/samcli/lib/pipeline/bootstrap/environment.py b/samcli/lib/pipeline/bootstrap/environment.py index 9d56e797bd..36cb318c77 100644 --- a/samcli/lib/pipeline/bootstrap/environment.py +++ b/samcli/lib/pipeline/bootstrap/environment.py @@ -7,6 +7,7 @@ import click from samcli.lib.config.samconfig import SamConfig +from samcli.lib.utils.colors import Colored from samcli.lib.utils.managed_cloudformation_stack import manage_stack, StackOutput from .resource import Resource, IAMUser, ECRImageRepository @@ -94,6 +95,7 @@ def __init__( self.artifacts_bucket: Resource = Resource(arn=artifacts_bucket_arn) self.create_image_repository: bool = create_image_repository self.image_repository: ECRImageRepository = ECRImageRepository(arn=image_repository_arn) + self.color = Colored() def did_user_provide_all_required_resources(self) -> bool: """Check if the user provided all of the environment resources or not""" @@ -138,7 +140,7 @@ def bootstrap(self, confirm_changeset: bool = True) -> bool: if self.did_user_provide_all_required_resources(): click.secho( - f"\nAll required resources for the {self.name} environment exist, skipping creation.", fg="yellow" + self.color.yellow(f"\nAll required resources for the {self.name} environment exist, skipping creation.") ) return True @@ -150,6 +152,7 @@ def bootstrap(self, confirm_changeset: bool = True) -> bool: if confirm_changeset: confirmed: bool = click.confirm("Should we proceed with the creation?") if not confirmed: + click.secho(self.color.red("Canceling pipeline bootstrap creation.")) return False environment_resources_template_body = Environment._read_template(ENVIRONMENT_RESOURCES_CFN_TEMPLATE) @@ -272,28 +275,30 @@ def print_resources_summary(self) -> None: created_resources.append(resource) if created_resources: - click.secho("\nWe have created the following resources:", fg="green") + click.secho(self.color.green("\nWe have created the following resources:")) for resource in created_resources: click.secho(f"\t{resource.arn}", fg="green") if provided_resources: click.secho( - "\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", - fg="green", + 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(f"\t{resource.arn}", fg="green") + click.secho(self.color.green(f"\t{resource.arn}")) if not self.pipeline_user.is_user_provided: click.secho( - "Please configure your CI/CD project with the following pipeline user credentials and " - "make sure to periodically rotate it:", - fg="green", + self.color.green( + "Please configure your CI/CD project with the following pipeline user credentials and " + "make sure to periodically rotate it:", + ) ) - click.secho(f"\tACCESS_KEY_ID: {self.pipeline_user.access_key_id}", fg="green") - click.secho(f"\tSECRET_ACCESS_KEY: {self.pipeline_user.secret_access_key}", fg="green") + click.secho(self.color.green(f"\tACCESS_KEY_ID: {self.pipeline_user.access_key_id}")) + click.secho(self.color.green(f"\tSECRET_ACCESS_KEY: {self.pipeline_user.secret_access_key}")) def _get_stack_name(self) -> str: sanitized_environment_name: str = re.sub("[^0-9a-zA-Z]+", "-", self.name) diff --git a/tests/integration/pipeline/test_bootstrap_command.py b/tests/integration/pipeline/test_bootstrap_command.py index 389295ae8a..9d94dd6d3d 100644 --- a/tests/integration/pipeline/test_bootstrap_command.py +++ b/tests/integration/pipeline/test_bootstrap_command.py @@ -206,7 +206,7 @@ def test_interactive_with_some_required_resources_provided(self): def test_interactive_pipeline_user_only_created_once(self): """ - Create 3 stages, only the first stage resource stack creates + Create 3 environments, only the first environment resource stack creates a pipeline user, and the remaining two share the same pipeline user. """ env_names = [] @@ -236,7 +236,7 @@ def test_interactive_pipeline_user_only_created_once(self): self.assertEqual(bootstrap_process_execute.process.returncode, 0) stdout = bootstrap_process_execute.stdout.decode() - # only first stage creates pipeline user + # Only first environment creates pipeline user if i == 0: self.assertIn("We have created the following resources", stdout) self.assertSetEqual( diff --git a/tests/integration/pipeline/test_init_command.py b/tests/integration/pipeline/test_init_command.py index 804249ea59..621bf32494 100644 --- a/tests/integration/pipeline/test_init_command.py +++ b/tests/integration/pipeline/test_init_command.py @@ -6,7 +6,6 @@ QUICK_START_JENKINS_INPUTS = [ "1", # quick start "1", # jenkins, this depends on the template repo. - "1", # two stage pipeline, this depends on the template repo. "credential-id", "main", "template.yaml", diff --git a/tests/unit/commands/pipeline/init/test_initeractive_init_flow.py b/tests/unit/commands/pipeline/init/test_initeractive_init_flow.py index 735d8bcff5..2f80694bf1 100644 --- a/tests/unit/commands/pipeline/init/test_initeractive_init_flow.py +++ b/tests/unit/commands/pipeline/init/test_initeractive_init_flow.py @@ -8,6 +8,8 @@ APP_PIPELINE_TEMPLATES_REPO_LOCAL_NAME, shared_path, CUSTOM_PIPELINE_TEMPLATE_REPO_LOCAL_NAME, + _prompt_cicd_provider, + _prompt_provider_pipeline_template, ) from samcli.commands.pipeline.init.pipeline_templates_manifest import AppPipelineTemplateManifestException from samcli.lib.utils.git_repo import CloneRepoException @@ -292,3 +294,37 @@ def test_generate_pipeline_configuration_file_from_custom_remote_pipeline_templa extra_context=cookiecutter_context_mock, overwrite_if_exists=True, ) + + @patch("samcli.lib.cookiecutter.question.click") + def test_prompt_cicd_provider_will_not_prompt_if_the_list_of_providers_has_only_one_provider(self, click_mock): + gitlab_provider = Mock(id="gitlab", display_name="Gitlab CI/CD") + providers = [gitlab_provider] + + chosen_provider = _prompt_cicd_provider(providers) + click_mock.prompt.assert_not_called() + self.assertEqual(chosen_provider, gitlab_provider) + + jenkins_provider = Mock(id="jenkins", display_name="Jenkins") + providers.append(jenkins_provider) + click_mock.prompt.return_value = "2" + chosen_provider = _prompt_cicd_provider(providers) + click_mock.prompt.assert_called_once() + self.assertEqual(chosen_provider, jenkins_provider) + + @patch("samcli.lib.cookiecutter.question.click") + def test_prompt_provider_pipeline_template_will_not_prompt_if_the_list_of_templatess_has_only_one_provider( + self, click_mock + ): + template1 = Mock(display_name="anyName1", location="anyLocation1", provider="a provider") + template2 = Mock(display_name="anyName2", location="anyLocation2", provider="a provider") + templates = [template1] + + chosen_template = _prompt_provider_pipeline_template(templates) + click_mock.prompt.assert_not_called() + self.assertEqual(chosen_template, template1) + + templates.append(template2) + click_mock.prompt.return_value = "2" + chosen_template = _prompt_provider_pipeline_template(templates) + click_mock.prompt.assert_called_once() + self.assertEqual(chosen_template, template2)