-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathinit_templates.py
226 lines (203 loc) · 10.6 KB
/
init_templates.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
"""
Manages the set of application templates.
"""
import itertools
import json
import os
import logging
import platform
import shutil
import subprocess
from pathlib import Path # must come after Py2.7 deprecation
import click
from samcli.cli.main import global_cfg
from samcli.commands.exceptions import UserException, AppTemplateUpdateException
from samcli.lib.utils import osutils
from samcli.lib.utils.osutils import rmtree_callback
from samcli.local.common.runtime_template import RUNTIME_DEP_TEMPLATE_MAPPING
LOG = logging.getLogger(__name__)
class InvalidInitTemplateError(UserException):
pass
class InitTemplates:
def __init__(self, no_interactive=False, auto_clone=True):
self._repo_url = "https://github.com/awslabs/aws-sam-cli-app-templates.git"
self._repo_name = "aws-sam-cli-app-templates"
self._temp_repo_name = "TEMP-aws-sam-cli-app-templates"
self.repo_path = None
self.clone_attempted = False
self._no_interactive = no_interactive
self._auto_clone = auto_clone
def prompt_for_location(self, runtime, dependency_manager):
options = self.init_options(runtime, 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"], "hello-world")
if template_md.get("directory") is not None:
return (os.path.join(self.repo_path, template_md["directory"]), template_md["appTemplate"])
raise InvalidInitTemplateError("Invalid template. This should not be possible, please raise an issue.")
def location_from_app_template(self, runtime, dependency_manager, app_template):
options = self.init_options(runtime, dependency_manager)
try:
template = next(item for item in options if self._check_app_template(item, app_template))
if template.get("init_location") is not None:
return template["init_location"]
if template.get("directory") is not None:
return os.path.join(self.repo_path, template["directory"])
raise InvalidInitTemplateError("Invalid template. This should not be possible, please raise an issue.")
except StopIteration as ex:
msg = "Can't find application template " + app_template + " - check valid values in interactive init."
raise InvalidInitTemplateError(msg) from ex
def _check_app_template(self, entry, app_template):
return entry["appTemplate"] == app_template
def init_options(self, runtime, dependency_manager):
if self.clone_attempted is False:
self._clone_repo()
if self.repo_path is None:
return self._init_options_from_bundle(runtime, dependency_manager)
return self._init_options_from_manifest(runtime, dependency_manager)
def _init_options_from_manifest(self, runtime, dependency_manager):
manifest_path = os.path.join(self.repo_path, "manifest.json")
with open(str(manifest_path)) as fp:
body = fp.read()
manifest_body = json.loads(body)
templates = manifest_body.get(runtime)
if templates is None:
# Fallback to bundled templates
return self._init_options_from_bundle(runtime, dependency_manager)
if dependency_manager is not None:
templates_by_dep = filter(lambda x: x["dependencyManager"] == dependency_manager, templates)
return list(templates_by_dep)
return templates
def _init_options_from_bundle(self, runtime, dependency_manager):
for mapping in list(itertools.chain(*(RUNTIME_DEP_TEMPLATE_MAPPING.values()))):
if runtime in mapping["runtimes"] or any([r.startswith(runtime) for r in mapping["runtimes"]]):
if not dependency_manager or dependency_manager == mapping["dependency_manager"]:
mapping["appTemplate"] = "hello-world" # when bundled, use this default template name
return [mapping]
msg = "Lambda Runtime {} and dependency manager {} does not have an available initialization template.".format(
runtime, dependency_manager
)
raise InvalidInitTemplateError(msg)
def _shared_dir_check(self, shared_dir):
try:
shared_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
return True
except OSError as ex:
LOG.warning("WARN: Unable to create shared directory.", exc_info=ex)
return False
def _clone_repo(self):
if not self._auto_clone:
return # Unit test escape hatch
# check if we have templates stored already
shared_dir = global_cfg.config_dir
if not self._shared_dir_check(shared_dir):
# Nothing we can do if we can't access the shared config directory, use bundled.
return
expected_path = os.path.normpath(os.path.join(shared_dir, self._repo_name))
if self._template_directory_exists(expected_path):
self._overwrite_existing_templates(shared_dir, expected_path)
else:
# simply create the app templates repo
self._clone_new_app_templates(shared_dir, expected_path)
self.clone_attempted = True
def _overwrite_existing_templates(self, shared_dir, expected_path):
self.repo_path = expected_path
# workflow to clone a copy to a new directory and overwrite
with osutils.mkdir_temp(ignore_errors=True) as tempdir:
try:
expected_temp_path = os.path.normpath(os.path.join(tempdir, self._repo_name))
LOG.info("\nCloning app templates from %s", self._repo_url)
subprocess.check_output(
[self._git_executable(), "clone", self._repo_url, self._repo_name],
cwd=tempdir,
stderr=subprocess.STDOUT,
)
# Now we need to delete the old repo and move this one.
self._replace_app_templates(expected_temp_path, expected_path)
self.repo_path = expected_path
except OSError as ex:
LOG.warning("WARN: Could not clone app template repo.", exc_info=ex)
except subprocess.CalledProcessError as clone_error:
output = clone_error.output.decode("utf-8")
if "not found" in output.lower():
click.echo("WARN: Could not clone app template repo.")
def _replace_app_templates(self, temp_path, dest_path):
try:
LOG.debug("Removing old templates from %s", str(dest_path))
shutil.rmtree(dest_path, onerror=rmtree_callback)
LOG.debug("Copying templates from %s to %s", str(temp_path), str(dest_path))
shutil.copytree(temp_path, dest_path, ignore=shutil.ignore_patterns("*.git"))
except (OSError, shutil.Error) as ex:
# UNSTABLE STATE - it's difficult to see how this scenario could happen except weird permissions, user will need to debug
raise AppTemplateUpdateException(
"Unstable state when updating app templates. Check that you have permissions to create/delete files in the AWS SAM shared directory or file an issue at https://github.com/awslabs/aws-sam-cli/issues"
) from ex
def _clone_new_app_templates(self, shared_dir, expected_path):
with osutils.mkdir_temp(ignore_errors=True) as tempdir:
expected_temp_path = os.path.normpath(os.path.join(tempdir, self._repo_name))
try:
LOG.info("\nCloning app templates from %s", self._repo_url)
subprocess.check_output(
[self._git_executable(), "clone", self._repo_url], cwd=tempdir, stderr=subprocess.STDOUT
)
shutil.copytree(expected_temp_path, expected_path, ignore=shutil.ignore_patterns("*.git"))
self.repo_path = expected_path
except OSError as ex:
LOG.warning("WARN: Can't clone app repo, git executable not found", exc_info=ex)
except subprocess.CalledProcessError as clone_error:
output = clone_error.output.decode("utf-8")
if "not found" in output.lower():
click.echo("WARN: Could not clone app template repo.")
def _template_directory_exists(self, expected_path):
path = Path(expected_path)
return path.exists()
def _git_executable(self):
execname = "git"
if platform.system().lower() == "windows":
options = [execname, "{}.cmd".format(execname), "{}.exe".format(execname), "{}.bat".format(execname)]
else:
options = [execname]
for name in options:
try:
subprocess.Popen([name], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# No exception. Let's pick this
return name
except OSError as ex:
LOG.debug("Unable to find executable %s", name, exc_info=ex)
raise OSError("Cannot find git, was looking at executables: {}".format(options))
def is_dynamic_schemas_template(self, app_template, runtime, dependency_manager):
"""
Check if provided template is dynamic template e.g: AWS Schemas template.
Currently dynamic templates require different handling e.g: for schema download and merge schema code in sam-app.
:param app_template:
:param runtime:
:param dependency_manager:
:return:
"""
options = self.init_options(runtime, dependency_manager)
for option in options:
if option.get("appTemplate") == app_template:
return option.get("isDynamicTemplate", False)
return False