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
156 changes: 93 additions & 63 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

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,85 @@ 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=None, app_template=None):
"""
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
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": {}}
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:
package_type = get_template_value("packageType", template)
use_case_name = get_template_value("useCaseName", template)
if not (package_type or use_case_name) or (app_template and app_template != template["appTemplate"]):
continue
runtime = get_runtime(package_type, template_runtime)
use_case = preprocessed_manifest.get(use_case_name, {})
use_case[runtime] = use_case.get(runtime, {})
use_case[runtime][package_type] = use_case[runtime].get(package_type, [])
use_case[runtime][package_type].append(template)

preprocessed_manifest[use_case_name] = use_case

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, template):
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, template_runtime):
if package_type == IMAGE:
template_runtime = re.split("/|-", template_runtime)[1]
return template_runtime
Loading