rostran/cli/__main__.py (315 lines of code) (raw):

""" Transforms, generates or formats ROS template. """ import json import os import logging import traceback from io import StringIO from pathlib import Path from typing import List, Set import typer from ruamel.yaml import YAML, YAMLError from rostran.core import exceptions from rostran.core.format import ( SourceTemplateFormat, TargetTemplateFormat, GeneratorFileFormat, convert_template_to_file_format, FileFormat, ) from rostran.core.rule_manager import RuleManager, RuleClassifier from rostran.core.template import RosTemplate yaml = YAML() yaml.preserve_quotes = True # cli app = typer.Typer(help=__doc__) SOURCE_TEMPLATE_FORMAT_DEFAULT = typer.Option( SourceTemplateFormat.Auto, help="Source template format" ) TARGET_TEMPLATE_FORMAT_DEFAULT = typer.Option( TargetTemplateFormat.Auto, help="Target template format." ) @app.command() def transform( source_path: str = typer.Argument( ..., help="The path of the source template file, which can be a template file in Excel, Terraform, " "AWS CloudFormation or AlibabaCloud ROS format.", ), source_format: SourceTemplateFormat = typer.Option( SourceTemplateFormat.Auto, "--source-format", "-S", show_default=False, help="The format of the source template file. The source file format is determined by the suffix " "of SOURCE_PATH by default. [default: auto]", ), target_path: str = typer.Option( None, "--target-path", "-t", help="The file path of the generated ROS or Terraform template. Default to current directory.", ), target_format: TargetTemplateFormat = typer.Option( TargetTemplateFormat.Auto, "--target-format", "-T", show_default=False, help="The generated template format. [default: auto]", ), compatible: bool = typer.Option( False, show_default=True, help="Whether to use compatible mode when transforming Terraform to ROS template. If compatible, " "keep the Terraform file content in the generated ROS template. Otherwise, it is transformed " "to a template using ROS syntax. This option is only available for Terraform template files.", ), force: bool = typer.Option( False, show_default=True, help="Whether to overwrite existing target file.", ), extra_files: List[str] = typer.Option( None, show_default=True, help="Add extra files, which supporting the specification of multiple files with fuzzy matching, " "when transforming Terraform to ROS template in compatible mode. " "This option is only available for Terraform template files in compatible mode.", ), ): """ Transform AWS CloudFormation/Terraform/Excel template to ROS template. SOURCE represents AWS CloudFormation/Terraform/Excel/ROS template file path which will be transformed from. If file extension is ".json/.yml/.yaml", it will be automatically regarded as AWS CloudFormation or ROS template. If file extension is ".xlsx", it will be automatically regarded as Excel template. TARGET represents ROS or Terraform template file path which will be transformed to. If not supplied, "template.yml" will be used for ROS, "terraform/alibabacloud" will be used for Terraform. """ # handle source template if not Path(source_path).exists(): raise exceptions.TemplateNotExist(path=source_path) tpl = None if source_format == SourceTemplateFormat.Auto: if source_path.endswith(".xlsx"): source_format = SourceTemplateFormat.Excel elif source_path.endswith(".tf"): source_format = SourceTemplateFormat.Terraform elif source_path.endswith((".json", ".yaml", ".yml")): try: from rostran.providers.ros.yaml_util import yaml with open(source_path, "r") as f: tpl = yaml.load(f) except YAMLError: raise exceptions.InvalidTemplate(path=source_path) ros_flag = tpl.get('ROSTemplateFormatVersion') == '2015-09-01' ros_tf = tpl.get("Transform") ros_tf_flag = False if isinstance(ros_tf, str) and ros_tf.startswith(("Aliyun::Terraform", "Aliyun::OpenTofu")): ros_tf_flag = True if ros_flag and ros_tf_flag: source_format = SourceTemplateFormat.ROSTerraform elif ros_flag and not ros_tf_flag: source_format = SourceTemplateFormat.ROS else: source_format = SourceTemplateFormat.CloudFormation else: raise exceptions.TemplateNotSupport(path=source_path) # handle target template if not target_path: if target_format == TargetTemplateFormat.Auto and source_format == SourceTemplateFormat.ROS: target_format = TargetTemplateFormat.Terraform target_path = "terraform/alicloud" elif target_format == TargetTemplateFormat.Terraform: target_path = "terraform/alicloud" elif target_format in (TargetTemplateFormat.Auto, TargetTemplateFormat.Yaml): target_path = "template.yml" target_format = TargetTemplateFormat.Yaml else: target_path = "template.json" if source_format == SourceTemplateFormat.ROSTerraform: target_path = "template" elif target_format == TargetTemplateFormat.Auto: if target_path.endswith((".yaml", ".yml")): target_format = TargetTemplateFormat.Yaml elif target_path.endswith(".json"): target_format = TargetTemplateFormat.Json else: if source_format not in (SourceTemplateFormat.ROS, SourceTemplateFormat.ROSTerraform): raise exceptions.TemplateNotSupport(path=target_path) target_path = os.path.abspath(target_path) path = Path(target_path) if path.exists() and not force: raise exceptions.TemplateAlreadyExist(path=target_path) if not path.parent.exists(): raise exceptions.PathNotExist(path=path.parent) # initialize template source_file_format = convert_template_to_file_format(source_format, source_path) if source_format == SourceTemplateFormat.Excel: from ..providers import ExcelTemplate template = ExcelTemplate.initialize(source_path, source_file_format) elif source_format == SourceTemplateFormat.Terraform: if compatible: from ..providers import CompatibleTerraformTemplate template = CompatibleTerraformTemplate.initialize( source_path, source_file_format, extra_files ) else: from ..providers import TerraformTemplate template = TerraformTemplate.initialize(source_path, source_file_format) elif source_format == SourceTemplateFormat.CloudFormation: from ..providers import CloudFormationTemplate template = CloudFormationTemplate.initialize(source_path, source_file_format) elif source_format == SourceTemplateFormat.ROSTerraform: from ..providers import WrapTerraformTemplate template = WrapTerraformTemplate.initialize(source_path, source_file_format) elif source_format == SourceTemplateFormat.ROS: from rostran.providers.ros.template import ROS2TerraformTemplate from rostran.providers.ros.yaml_util import yaml if tpl is None: if not source_path or not source_path.endswith((".json", ".yaml", ".yml")): raise exceptions.TemplateNotSupport(path=source_path) try: with open(source_path, "r") as f: tpl = yaml.load(f) except YAMLError: raise exceptions.TemplateNotSupport(path=source_path) template = ROS2TerraformTemplate.initialize(tpl) else: raise exceptions.TemplateNotSupport(path=source_path) # transform template if source_format in (SourceTemplateFormat.ROSTerraform, SourceTemplateFormat.ROS): template.transform(target_path) else: ros_templates = template.transform() if not isinstance(ros_templates, list): ros_templates.save(target_path, target_format) elif len(ros_templates) == 1: ros_templates[0].save(target_path, target_format) else: for i, ros_template in enumerate(ros_templates): name_parts = os.path.splitext(target_path) path = f"{name_parts[0]}-{i}{name_parts[1]}" ros_template.save(path, target_format) @app.command() def format( path: List[Path] = typer.Argument( ..., exists=True, resolve_path=True, help="The path of ROS template file to format.", ), replace: bool = typer.Option( False, help="Whether replace the content of the source file with the formatted content.", ), skip: List[Path] = typer.Option( None, exists=True, resolve_path=True, help="The path of ROS Template file that need to skip formatting.", ), ): """ Format and check ROS template according to the standard specification. """ ps = [] ps_set = set() skip_set = set(skip or []) for p in path: if p not in ps_set and p not in skip_set: ps_set.add(p) ps.append(p) formatted_paths = [] for p in ps: if not p.exists(): raise exceptions.PathNotExist(path=path) if p in skip_set: continue if p.is_dir(): r = _format_directory(p, replace, skip_set) if r: formatted_paths.extend(r) else: r = _format_file(p, replace) if r: formatted_paths.append(r) if formatted_paths: typer.secho("Formatted successfully.", fg="green") else: typer.secho("No templates were found that could be formatted.", fg="yellow") def _format_file(path: Path, replace: bool = False, check_suffix=True): suffix = path.suffix if suffix == ".json": file_format = FileFormat.Json try: source = json.loads(path.read_text()) except json.JSONDecodeError: raise exceptions.InvalidTemplateFormat(path=path, format="json") elif suffix in (".yaml", ".yml"): file_format = FileFormat.Yaml try: source = yaml.load(path.read_text()) except YAMLError: raise exceptions.InvalidTemplateFormat(path=path, format="yaml") else: if check_suffix: raise exceptions.TemplateFormatNotSupport(path=path, format=suffix) return typer.secho(f"Formatting {path}.", fg="green") template = RosTemplate.initialize(source) data = template.as_dict(format=True) if file_format == FileFormat.Json: content = json.dumps(data, indent=2, ensure_ascii=False) else: s = StringIO() yaml.dump(data, s) content = s.getvalue() if replace: with path.open("w") as f: f.write(content) else: typer.secho(content) typer.echo() return path def _format_directory( path: Path, replace: bool = False, skip_paths: Set[Path] = None ) -> list: formatted_paths = [] for sub_path in path.iterdir(): if skip_paths and sub_path in skip_paths: continue if sub_path.is_dir(): r = _format_directory(sub_path, replace, skip_paths) if r: formatted_paths.extend(r) else: r = _format_file(sub_path, replace, check_suffix=False) if r: formatted_paths.append(r) return formatted_paths @app.command() def rules( terraform: bool = typer.Option( True, help="Whether to show Terraform transform rules.", ), cloudformation: bool = typer.Option( True, help="Whether to show AWS CloudFormation transform rules.", ), markdown: bool = typer.Option( False, help="Whether to show rules in markdown format.", ), with_link: bool = typer.Option( False, help="Whether to include links when showing rules in markdown format.", ), ): """ Show transform rules of Terraform and CloudFormation. """ newline = False if terraform: rule_manager = RuleManager.initialize(RuleClassifier.TerraformAliCloud) rule_manager.show(markdown, with_link) newline = True if cloudformation: if newline: typer.echo("") rule_manager = RuleManager.initialize(RuleClassifier.CloudFormation) rule_manager.show(markdown, with_link) def main(): logging.basicConfig(level=logging.INFO, format="%(message)s") try: typer.main.get_command(app)(prog_name="rostran") except exceptions.RosTranWarning as e: typer.secho(f"{e}", fg=typer.colors.YELLOW) typer.Exit(1) except exceptions.RosTranException as e: typer.secho(f"{e}", fg=typer.colors.RED) typer.Exit(2) except Exception: typer.secho(traceback.format_exc(), fg=typer.colors.RED) typer.Exit(3) if __name__ == "__main__": main()