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)