github.com/aws-cloudformation/cloudformation-cli-go-plugin@v1.2.0/python/rpdk/go/codegen.py (about) 1 # pylint: disable=useless-super-delegation,too-many-locals 2 # pylint doesn't recognize abstract methods 3 import logging 4 import zipfile 5 from pathlib import Path 6 from rpdk.core.data_loaders import resource_stream 7 from rpdk.core.exceptions import DownstreamError, InternalError, SysExitRecommendedError 8 from rpdk.core.init import input_with_validation 9 from rpdk.core.jsonutils.resolver import resolve_models 10 from rpdk.core.plugin_base import LanguagePlugin 11 from rpdk.core.project import Project 12 from subprocess import PIPE, CalledProcessError, run as subprocess_run # nosec 13 from tempfile import TemporaryFile 14 15 from .resolver import translate_type 16 from .utils import safe_reserved, validate_path 17 18 LOG = logging.getLogger(__name__) 19 20 OPERATIONS = ("Create", "Read", "Update", "Delete", "List") 21 EXECUTABLE = "cfn-cli" 22 23 LANGUAGE = "go" 24 25 DEFAULT_SETTINGS = {"protocolVersion": "2.0.0"} 26 27 28 class GoExecutableNotFoundError(SysExitRecommendedError): 29 pass 30 31 32 class GoLanguagePlugin(LanguagePlugin): 33 MODULE_NAME = __name__ 34 NAME = "go" 35 RUNTIME = "go1.x" 36 ENTRY_POINT = "handler" 37 TEST_ENTRY_POINT = "handler" 38 CODE_URI = "bin/" 39 40 def __init__(self): 41 self.env = self._setup_jinja_env( 42 trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True 43 ) 44 self.env.filters["translate_type"] = translate_type 45 self.env.filters["safe_reserved"] = safe_reserved 46 self._use_docker = None 47 self._protocol_version = "2.0.0" 48 self.import_path = "" 49 50 def _prompt_for_go_path(self, project): 51 path_validator = validate_path("") 52 import_path = path_validator(project.settings.get("import_path")) 53 54 if not import_path: 55 prompt = "Enter the GO Import path" 56 import_path = input_with_validation(prompt, path_validator) 57 58 self.import_path = import_path 59 project.settings["import_path"] = str(self.import_path) 60 61 def init(self, project: Project): 62 LOG.debug("Init started") 63 64 self._prompt_for_go_path(project) 65 66 self._init_settings(project) 67 68 # .gitignore 69 path = project.root / ".gitignore" 70 LOG.debug("Writing .gitignore: %s", path) 71 contents = resource_stream(__name__, "data/go.gitignore").read() 72 project.safewrite(path, contents) 73 74 # project folder structure 75 src = project.root / "cmd" / "resource" 76 LOG.debug("Making source folder structure: %s", src) 77 src.mkdir(parents=True, exist_ok=True) 78 79 inter = project.root / "internal" 80 inter.mkdir(parents=True, exist_ok=True) 81 82 # Makefile 83 path = project.root / "Makefile" 84 LOG.debug("Writing Makefile: %s", path) 85 template = self.env.get_template("Makefile") 86 contents = template.render() 87 project.overwrite(path, contents) 88 89 # go.mod 90 path = project.root / "go.mod" 91 LOG.debug("Writing go.mod: %s", path) 92 template = self.env.get_template("go.mod.tple") 93 contents = template.render(path=Path(project.settings["import_path"])) 94 project.safewrite(path, contents) 95 96 # CloudFormation/SAM template for handler lambda 97 path = project.root / "template.yml" 98 LOG.debug("Writing SAM template: %s", path) 99 template = self.env.get_template("template.yml") 100 handler_params = { 101 "Handler": project.entrypoint, 102 "Runtime": project.runtime, 103 "CodeUri": self.CODE_URI, 104 } 105 test_handler_params = { 106 "Handler": project.entrypoint, 107 "Runtime": project.runtime, 108 "CodeUri": self.CODE_URI, 109 "Environment": "", 110 " Variables": "", 111 " MODE": "Test", 112 } 113 contents = template.render( 114 resource_type=project.type_name, 115 functions={ 116 "TypeFunction": handler_params, 117 "TestEntrypoint": test_handler_params, 118 }, 119 ) 120 project.safewrite(path, contents) 121 122 LOG.debug("Writing handlers and tests") 123 self.init_handlers(project, src) 124 125 # README 126 path = project.root / "README.md" 127 LOG.debug("Writing README: %s", path) 128 template = self.env.get_template("README.md") 129 contents = template.render( 130 type_name=project.type_name, 131 schema_path=project.schema_path, 132 executable=EXECUTABLE, 133 files="model.go and main.go", 134 ) 135 project.safewrite(path, contents) 136 137 LOG.debug("Init complete") 138 139 def _init_settings(self, project: Project): 140 project.runtime = self.RUNTIME 141 project.entrypoint = self.ENTRY_POINT.format(self.import_path) 142 project.test_entrypoint = self.TEST_ENTRY_POINT.format(self.import_path) 143 project.settings.update(DEFAULT_SETTINGS) 144 if project.settings.get("use_docker"): 145 self._use_docker = True 146 else: 147 self._use_docker = False 148 project.settings["protocolVersion"] = self._protocol_version 149 150 def init_handlers(self, project: Project, src): 151 LOG.debug("Writing stub handlers") 152 template = self.env.get_template("stubHandler.go.tple") 153 path = src / "resource.go" 154 contents = template.render() 155 project.safewrite(path, contents) 156 157 # pylint: disable=unused-argument,no-self-use 158 def _get_generated_root(self, project: Project): 159 LOG.debug("Init started") 160 161 def generate(self, project: Project): 162 LOG.debug("Generate started") 163 root = project.root / "cmd" 164 165 # project folder structure 166 src = root / "resource" 167 format_paths = [] 168 169 LOG.debug("Writing Types") 170 171 models = resolve_models(project.schema) 172 if project.configuration_schema: 173 configuration_schema_path = ( 174 project.root / project.configuration_schema_filename 175 ) 176 project.write_configuration_schema(configuration_schema_path) 177 configuration_models = resolve_models( 178 project.configuration_schema, "TypeConfiguration" 179 ) 180 else: 181 configuration_models = {"TypeConfiguration": {}} 182 183 # Create the type configuration model 184 template = self.env.get_template("config.go.tple") 185 path = src / "config.go" 186 contents = template.render(models=configuration_models) 187 project.overwrite(path, contents) 188 format_paths.append(path) 189 190 # Create the resource model 191 template = self.env.get_template("types.go.tple") 192 path = src / "model.go" 193 contents = template.render(models=models) 194 project.overwrite(path, contents) 195 format_paths.append(path) 196 197 path = root / "main.go" 198 LOG.debug("Writing project: %s", path) 199 template = self.env.get_template("main.go.tple") 200 importpath = Path(project.settings["import_path"]) 201 contents = template.render(path=(importpath / "cmd" / "resource").as_posix()) 202 project.overwrite(path, contents) 203 format_paths.append(path) 204 205 # makebuild 206 path = project.root / "makebuild" 207 LOG.debug("Writing makebuild: %s", path) 208 template = self.env.get_template("makebuild") 209 contents = template.render() 210 project.overwrite(path, contents) 211 212 # named files must all be in one directory 213 for path in format_paths: 214 try: 215 subprocess_run( 216 ["go", "fmt", path], cwd=root, check=True, stdout=PIPE, stderr=PIPE 217 ) # nosec 218 except (FileNotFoundError, CalledProcessError) as e: 219 raise DownstreamError("go fmt failed") from e 220 221 # Update settings as needed 222 need_to_write = False 223 for key, new in DEFAULT_SETTINGS.items(): 224 old = project.settings.get(key) 225 226 if project.settings.get(key) != new: 227 LOG.debug( 228 "{key} version change from {old} to {new}", 229 key=key, 230 old=old, 231 new=new, 232 ) 233 project.settings[key] = new 234 need_to_write = True 235 236 if need_to_write: 237 project.write_settings() 238 239 @staticmethod 240 def pre_package(project: Project): 241 # zip the Go build output - it's all needed to execute correctly 242 with TemporaryFile("w+b") as f: 243 with zipfile.ZipFile(f, mode="w") as zip_file: 244 for path in (project.root / "bin").iterdir(): 245 if path.is_file(): 246 zip_file.write(path.resolve(), path.name) 247 f.seek(0) 248 249 return f 250 251 @staticmethod 252 def _find_exe(project: Project): 253 exe_glob = list((project.root / "bin").glob("handler")) 254 if not exe_glob: 255 LOG.debug("No Go executable match") 256 raise GoExecutableNotFoundError( 257 "You must build the handler before running cfn-submit.\n" 258 "Please run 'make' or the equivalent command " 259 "in your IDE to compile and package the code." 260 ) 261 262 if len(exe_glob) > 1: 263 LOG.debug( 264 "Multiple Go executable match: %s", 265 ", ".join(str(path) for path in exe_glob), 266 ) 267 raise InternalError("Multiple Go executable match") 268 269 LOG.debug("Generate complete") 270 return exe_glob[0] 271 272 def package(self, project: Project, zip_file): 273 LOG.info("Packaging Go project") 274 275 def write_with_relative_path(path): 276 relative = path.relative_to(project.root) 277 zip_file.write(path.resolve(), str(relative)) 278 279 # sanity check for build output 280 self._find_exe(project) 281 282 executable_zip = self.pre_package(project) 283 zip_file.writestr("handler.zip", executable_zip.read()) 284 285 write_with_relative_path(project.root / "Makefile") 286 287 for path in (project.root / "cmd").rglob("*"): 288 if path.is_file(): 289 write_with_relative_path(path) 290 291 for path in (project.root / "internal").rglob("*"): 292 if path.is_file(): 293 write_with_relative_path(path)