diff --git a/samcli/commands/pipeline/init/interactive_init_flow.py b/samcli/commands/pipeline/init/interactive_init_flow.py index 2efe5d6dc1..c084a65ed8 100644 --- a/samcli/commands/pipeline/init/interactive_init_flow.py +++ b/samcli/commands/pipeline/init/interactive_init_flow.py @@ -96,11 +96,25 @@ def _load_pipeline_bootstrap_context() -> Dict: config = SamConfig(PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME) if not config.exists(): + context[str(["environment_names_message"])] = "" return context - for env in config.get_env_names(): + + # config.get_env_names() will return the list of + # bootstrapped env names and "default" which is used to store shared values + # we don't want to include "default" here. + env_names = [env_name for env_name in config.get_env_names() if env_name != "default"] + for env in env_names: for key, value in config.get_all(bootstrap_command_names, section, env).items(): context[str([env, key])] = value + # pre-load the list of env names detected from pipelineconfig.toml + environment_names_message = ( + "Here are the environment names detected " + + f"in {os.path.join(PIPELINE_CONFIG_DIR, PIPELINE_CONFIG_FILENAME)}:\n" + + "\n".join([f"\t- {env_name}" for env_name in env_names]) + ) + context[str(["environment_names_message"])] = environment_names_message + return context diff --git a/samcli/lib/cookiecutter/question.py b/samcli/lib/cookiecutter/question.py index 9cbb67f05d..fbec641388 100644 --- a/samcli/lib/cookiecutter/question.py +++ b/samcli/lib/cookiecutter/question.py @@ -108,7 +108,7 @@ def ask(self, context: Optional[Dict] = None) -> Any: # 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) + return click.prompt(text=self._resolve_text(context), default=resolved_default_answer) def get_next_question_key(self, answer: Any) -> Optional[str]: # _next_question_map is a Dict[str(answer), str(next question key)] @@ -154,51 +154,59 @@ def _resolve_key_path(self, key_path: List, context: Dict) -> List[str]: raise ValueError(f'Invalid value "{unresolved_key}" in key path') return resolved_key_path - def _resolve_default_answer(self, context: Optional[Dict] = None) -> Optional[Any]: + def _resolve_value_from_expression(self, expression: Any, context: Optional[Dict] = None) -> Optional[Any]: """ - a question may have a default answer provided directly through the "default_answer" value + a question may have a value provided directly as string or number value or indirectly from cookiecutter context using a key path Parameters ---------- context - Cookiecutter context used to resolve default values and answered questions' answers. + Cookiecutter context used to resolve values. Raises ------ KeyError - When default value depends on the answer to a non-existent question + When an expression depends on the answer to a non-existent question ValueError - The default value is malformed + The expression is malformed Returns ------- - Optional default answer, it might be resolved from cookiecutter context using specified key path. + Optional value, it might be resolved from cookiecutter context using specified key path. """ - if isinstance(self._default_answer, dict): + if isinstance(expression, dict): context = context if context else {} # load value using key path from cookiecutter - if "keyPath" not in self._default_answer: - raise KeyError(f'Missing key "keyPath" in question default "{self._default_answer}".') - unresolved_key_path = self._default_answer.get("keyPath", []) + if "keyPath" not in expression: + raise KeyError(f'Missing key "keyPath" in "{expression}".') + unresolved_key_path = expression.get("keyPath", []) if not isinstance(unresolved_key_path, list): - raise ValueError(f'Invalid default answer "{self._default_answer}" for question {self.key}') + raise ValueError(f'Invalid expression "{expression}" in question {self.key}') return context.get(str(self._resolve_key_path(unresolved_key_path, context))) + return expression + + def _resolve_text(self, context: Optional[Dict] = None) -> str: + resolved_text = self._resolve_value_from_expression(self._text, context) + if resolved_text is None: + raise ValueError(f"Cannot resolve value from expression: {self._text}") + return str(resolved_text) - return self._default_answer + def _resolve_default_answer(self, context: Optional[Dict] = None) -> Optional[Any]: + return self._resolve_value_from_expression(self._default_answer, context) class Info(Question): def ask(self, context: Optional[Dict] = None) -> None: - return click.echo(message=self._text) + return click.echo(message=self._resolve_text(context)) class Confirm(Question): def ask(self, context: Optional[Dict] = None) -> bool: - return click.confirm(text=self._text) + return click.confirm(text=self._resolve_text(context)) class Choice(Question): @@ -219,7 +227,7 @@ def __init__( def ask(self, context: Optional[Dict] = None) -> str: resolved_default_answer = self._resolve_default_answer(context) - click.echo(self._text) + click.echo(self._resolve_text(context)) for index, option in enumerate(self._options): click.echo(f"\t{index + 1} - {option}") options_indexes = self._get_options_indexes(base=1) 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 6bb0e5f89d..446cbdc6cf 100644 --- a/tests/unit/commands/pipeline/init/test_initeractive_init_flow.py +++ b/tests/unit/commands/pipeline/init/test_initeractive_init_flow.py @@ -171,6 +171,8 @@ def test_generate_pipeline_configuration_file_from_app_pipeline_template_happy_c { str(["testing", "pipeline_execution_role"]): "arn:aws:iam::123456789012:role/execution-role", str(["prod", "pipeline_execution_role"]): "arn:aws:iam::123456789012:role/execution-role", + str(["environment_names_message"]): "Here are the environment names detected " + f'in {os.path.join(".aws-sam", "pipeline", "pipelineconfig.toml")}:\n\t- testing\n\t- prod', } ) cookiecutter_mock.assert_called_once_with(