Skip to content
Merged
6 changes: 6 additions & 0 deletions samcli/commands/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ class AppPipelineTemplateMetadataException(UserException):
"""


class InvalidInitOptionException(UserException):
"""
Exception class when user provides wrong options
"""


class InvalidImageException(UserException):
"""
Value provided to --build-image or --invoke-image is invalid URI
Expand Down
8 changes: 4 additions & 4 deletions samcli/commands/init/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from samcli.lib.utils.version_checker import check_newer_version
from samcli.local.common.runtime_template import RUNTIMES, SUPPORTED_DEP_MANAGERS, LAMBDA_IMAGES_RUNTIMES
from samcli.lib.telemetry.metric import track_command
from samcli.commands.init.interactive_init_flow import _get_runtime_from_image, get_architectures
from samcli.commands.init.interactive_init_flow import _get_runtime_from_image, get_architectures, get_sorted_runtimes
from samcli.commands.local.cli_common.click_mutex import Mutex
from samcli.lib.utils.packagetype import IMAGE, ZIP
from samcli.lib.utils.architecture import X86_64, ARM64
Expand Down Expand Up @@ -152,7 +152,7 @@ def wrapped(*args, **kwargs):
@click.option(
"-r",
"--runtime",
type=click.Choice(RUNTIMES),
type=click.Choice(get_sorted_runtimes(RUNTIMES)),
help="Lambda Runtime of your app",
cls=Mutex,
not_required=["location", "base_image"],
Expand Down Expand Up @@ -286,9 +286,9 @@ def do_cli(
image_bool = name and pt_explicit and base_image
if location or zip_bool or image_bool:
# need to turn app_template into a location before we generate
templates = InitTemplates(no_interactive)
templates = InitTemplates()
if package_type == IMAGE and image_bool:
base_image, runtime = _get_runtime_from_image(base_image)
runtime = _get_runtime_from_image(base_image)
options = templates.init_options(package_type, runtime, base_image, dependency_manager)
if not app_template:
if len(options) == 1:
Expand Down
199 changes: 135 additions & 64 deletions samcli/commands/init/init_templates.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
"""
Manages the set of application templates.
"""

import re
import itertools
import json
import logging
import os
from pathlib import Path
from typing import Dict
from typing import Dict, Optional

import click
from samcli.cli.global_config import GlobalConfig

from samcli.commands.exceptions import UserException, AppTemplateUpdateException
from samcli.lib.utils.git_repo import GitRepo, CloneRepoException, CloneRepoUnstableStateException
from samcli.lib.utils.packagetype import IMAGE
Expand All @@ -27,62 +25,9 @@ class InvalidInitTemplateError(UserException):


class InitTemplates:
def __init__(self, no_interactive=False):
self._no_interactive = no_interactive
def __init__(self):
self._git_repo: GitRepo = GitRepo(url=APP_TEMPLATES_REPO_URL)

def prompt_for_location(self, package_type, runtime, base_image, dependency_manager):
"""
Prompt for template location based on other information provided in previous steps.

Parameters
----------
package_type : str
the package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py
runtime : str
the runtime string
base_image : str
the base image string
dependency_manager : str
the dependency manager string

Returns
-------
location : str
The location of the template
app_template : str
The name of the template
"""
options = self.init_options(package_type, runtime, base_image, dependency_manager)

if len(options) == 1:
template_md = options[0]
else:
choices = list(map(str, range(1, len(options) + 1)))
choice_num = 1
click.echo("\nAWS quick start application templates:")
for o in options:
if o.get("displayName") is not None:
msg = "\t" + str(choice_num) + " - " + o.get("displayName")
click.echo(msg)
else:
msg = (
"\t"
+ str(choice_num)
+ " - Default Template for runtime "
+ runtime
+ " with dependency manager "
+ dependency_manager
)
click.echo(msg)
choice_num = choice_num + 1
choice = click.prompt("Template selection", type=click.Choice(choices), show_choices=False)
template_md = options[int(choice) - 1] # zero index
if template_md.get("init_location") is not None:
return (template_md["init_location"], template_md["appTemplate"])
if template_md.get("directory") is not None:
return os.path.join(self._git_repo.local_path, template_md["directory"]), template_md["appTemplate"]
raise InvalidInitTemplateError("Invalid template. This should not be possible, please raise an issue.")
self.manifest_file_name = "manifest.json"

def location_from_app_template(self, package_type, runtime, base_image, dependency_manager, app_template):
options = self.init_options(package_type, runtime, base_image, dependency_manager)
Expand All @@ -104,23 +49,26 @@ def _check_app_template(entry: Dict, app_template: str) -> bool:
return bool(entry["appTemplate"] == app_template)

def init_options(self, package_type, runtime, base_image, dependency_manager):
self.clone_templates_repo()
if self._git_repo.local_path is None:
return self._init_options_from_bundle(package_type, runtime, dependency_manager)
return self._init_options_from_manifest(package_type, runtime, base_image, dependency_manager)

def clone_templates_repo(self):
if not self._git_repo.clone_attempted:
shared_dir: Path = GlobalConfig().config_dir
try:
self._git_repo.clone(clone_dir=shared_dir, clone_name=APP_TEMPLATES_REPO_NAME, replace_existing=True)
except CloneRepoUnstableStateException as ex:
raise AppTemplateUpdateException(str(ex)) from ex
except (OSError, CloneRepoException):
# If can't clone, try using an old clone from a previous run if already exist
LOG.debug("Clone error, attempting to use an old clone from a previous run")
expected_previous_clone_local_path: Path = shared_dir.joinpath(APP_TEMPLATES_REPO_NAME)
if expected_previous_clone_local_path.exists():
self._git_repo.local_path = expected_previous_clone_local_path
if self._git_repo.local_path is None:
return self._init_options_from_bundle(package_type, runtime, dependency_manager)
return self._init_options_from_manifest(package_type, runtime, base_image, dependency_manager)

def _init_options_from_manifest(self, package_type, runtime, base_image, dependency_manager):
manifest_path = os.path.join(self._git_repo.local_path, "manifest.json")
manifest_path = self.get_manifest_path()
with open(str(manifest_path)) as fp:
body = fp.read()
manifest_body = json.loads(body)
Expand Down Expand Up @@ -171,3 +119,126 @@ def is_dynamic_schemas_template(self, package_type, app_template, runtime, base_
if option.get("appTemplate") == app_template:
return option.get("isDynamicTemplate", False)
return False

def get_app_template_location(self, template_directory):
return os.path.normpath(os.path.join(self._git_repo.local_path, template_directory))

def get_manifest_path(self):
return Path(self._git_repo.local_path, self.manifest_file_name)

def get_preprocessed_manifest(
self,
filter_value: Optional[str] = None,
app_template: Optional[str] = None,
package_type: Optional[str] = None,
dependency_manager: Optional[str] = None,
) -> dict:
"""
This method get the manifest cloned from the git repo and preprocessed it.
Below is the link to manifest:
https://github.com/aws/aws-sam-cli-app-templates/blob/master/manifest.json
The structure of the manifest is shown below:
{
"dotnetcore2.1": [
{
"directory": "dotnetcore2.1/cookiecutter-aws-sam-hello-dotnet",
"displayName": "Hello World Example",
"dependencyManager": "cli-package",
"appTemplate": "hello-world",
"packageType": "Zip",
"useCaseName": "Serverless API"
},
]
}
Parameters
----------
filter_value : string, optional
This could be a runtime or a base-image, by default None
app_template : string, optional
Application template generated
package_type : string, optional
The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py
dependency_manager : string, optional
dependency manager
Returns
-------
[dict]
This is preprocessed manifest with the use_case as key
"""
self.clone_templates_repo()
manifest_path = self.get_manifest_path()
with open(str(manifest_path)) as fp:
body = fp.read()
manifest_body = json.loads(body)

# This would ensure the Use-Case Hello World Example appears
# at the top of list template example displayed to the Customer.
preprocessed_manifest = {"Hello World Example": {}} # type: dict
for template_runtime in manifest_body:
if filter_value and filter_value != template_runtime:
continue
template_list = manifest_body[template_runtime]
for template in template_list:
template_package_type = get_template_value("packageType", template)
use_case_name = get_template_value("useCaseName", template)
if not (template_package_type or use_case_name) or template_does_not_meet_filter_criteria(
app_template, package_type, dependency_manager, template
):
continue
runtime = get_runtime(template_package_type, template_runtime)
use_case = preprocessed_manifest.get(use_case_name, {})
use_case[runtime] = use_case.get(runtime, {})
use_case[runtime][template_package_type] = use_case[runtime].get(template_package_type, [])
use_case[runtime][template_package_type].append(template)

preprocessed_manifest[use_case_name] = use_case

if not bool(preprocessed_manifest["Hello World Example"]):
del preprocessed_manifest["Hello World Example"]

return preprocessed_manifest

def get_bundle_option(self, package_type, runtime, dependency_manager):
return self._init_options_from_bundle(package_type, runtime, dependency_manager)


def get_template_value(value: str, template: dict) -> Optional[str]:
if value not in template:
LOG.debug(
f"Template is missing the value for {value} in manifest file. Please raise a github issue."
+ f" Template details: {template}"
)
return template.get(value)


def get_runtime(package_type: Optional[str], template_runtime: str) -> str:
if package_type == IMAGE:
template_runtime = re.split("/|-", template_runtime)[1]
return template_runtime


def template_does_not_meet_filter_criteria(
app_template: Optional[str], package_type: Optional[str], dependency_manager: Optional[str], template: dict
) -> bool:
"""
Parameters
----------
app_template : Optional[str]
Application template generated
package_type : Optional[str]
The package type, 'Zip' or 'Image', see samcli/lib/utils/packagetype.py
dependency_manager : Optional[str]
Dependency manager
template : dict
key-value pair app template configuration

Returns
-------
bool
True if template does not meet filter criteria else False
"""
return bool(
(app_template and app_template != template.get("appTemplate"))
or (package_type and package_type != template.get("packageType"))
or (dependency_manager and dependency_manager != template.get("dependencyManager"))
)
Loading