rostran/providers/terraform/template_blocks.py (190 lines of code) (raw):
import json
from abc import abstractmethod
from typing import Any, Dict, List, Optional, Tuple, Union
from dataclasses import dataclass
try:
from typing import Protocol, runtime_checkable
except ImportError:
from typing_extensions import Protocol, runtime_checkable
@runtime_checkable
class TerraformType(Protocol):
value: Any
def __str__(self) -> str:
return self.render()
@classmethod
@abstractmethod
def render(cls, indent: int = 0) -> str:
raise NotImplementedError
@dataclass
class LiteralType(TerraformType):
value: Any
def render(self, _=0):
return self.value
@dataclass(frozen=True)
class QuotedString(TerraformType):
value: str
def render(self, _=0):
if '"' in self.value:
value = self.value.replace('"', '\\"')
return f'"{value}"'
return f'"{self.value}"'
@dataclass(frozen=True)
class NumberType(TerraformType):
value: Union[int, float]
def render(self, _=0):
return str(self.value)
@dataclass(frozen=True)
class NullType(TerraformType):
value: str = "null"
def render(self, _=0):
return self.value
def __eq__(self, other: object) -> bool:
return other == self.value
@dataclass(frozen=True)
class BooleanType(TerraformType):
value: bool
def render(self, _=0):
return str(self.value).lower()
@dataclass
class JsonType(TerraformType):
value: Union[List[Union[TerraformType, "Block"]], Dict[Union[QuotedString, LiteralType, str], TerraformType]]
def render(self, indent=0):
suffix = " " * indent
indent += 2
space = " " * indent
if isinstance(self.value, list):
result = "[\n"
for item in self.value:
comma = "" if item is self.value[-1] else ","
if not isinstance(item, TerraformType):
item = LiteralType(item)
result += f"{space}{item.render(indent)}{comma}\n"
result = result.rstrip() + f"\n{suffix}]"
elif isinstance(self.value, dict):
if not self.value:
return "{}"
result = "{"
for name, value in self.value.items():
if not isinstance(value, TerraformType):
value = LiteralType(value)
result += f"\n{space}{name} = {value.render(indent)}"
result = result.rstrip() + f"\n{suffix}}}"
else:
result = str(self.value)
return result
@dataclass
class CommentType(TerraformType):
value: str
def render(self, indent=0):
spacing = " " * indent
text = [f"{spacing}// {line.rstrip()}" for line in self.value.splitlines()]
return "\n".join(text)
def convert_to_tf_type(value: Any, type_: Optional[str] = None) -> TerraformType:
if isinstance(value, TerraformType):
return value
if type_ is None:
if isinstance(value, str):
return QuotedString(value)
elif isinstance(value, bool):
return BooleanType(value)
elif isinstance(value, (int, float)):
return NumberType(value)
elif value is None:
return NullType()
elif isinstance(value, (dict, list)):
return JsonType(value)
return LiteralType(value)
if type_ == "string":
return QuotedString(value)
elif type_ == "number":
return NumberType(value)
elif type_ == "boolean":
return BooleanType(value)
elif type_ == "null":
return NullType()
elif type_ in ("json", "dict", "list"):
return JsonType(value)
elif type_ == "comment":
return CommentType(value)
elif type_:
return LiteralType(value)
BlockLabel = Tuple[Union[str, QuotedString,], ...]
Arguments = Dict[str, Union[TerraformType, "Block"]]
class Block:
def __init__(
self,
block_type: str,
labels: Optional[BlockLabel] = None,
arguments: Optional[Arguments] = None,
) -> None:
self.block_type = block_type
self.labels = labels if labels else ()
self.arguments = arguments if arguments else {}
def base_ref(self):
return f"{self.block_type}.{'.'.join(str(self.labels))}".replace('"', "")
def __str__(self) -> str:
return self.render()
def render(self, indent=0):
space = " " * indent
indent += 2
labels = " ".join(str(label) for label in self.labels)
label_space = " " if self.labels else ""
newline = ""
args = ""
if self.arguments:
newline = "\n" if self.arguments else ""
indent_spacing = " " * indent
block_args = []
comment_args = []
json_args = []
common_args = []
specific_args = []
for name, value in self.arguments.items():
if self.block_type == "resource" and name == "count":
specific_args.append(f"{indent_spacing}{name} = {value.render(indent)}")
continue
if isinstance(value, Block):
block_args.append(value.render(indent))
elif isinstance(value, CommentType):
comment_args.append(value.render(indent))
elif isinstance(value, JsonType):
json_args.append(f"{indent_spacing}{name} = {value.render(indent)}")
elif isinstance(value, TerraformType):
common_args.append(f"{indent_spacing}{name} = {value.render(indent)}")
else:
common_args.append(f"{indent_spacing}{name} = {value}")
all_args = specific_args + comment_args + common_args + json_args + block_args
args = "\n".join(all_args)
return f"{space}{self.block_type}{label_space}{labels} {{{newline}{args}{newline}{space}}}"
class Variable(Block):
def __init__(self, name: str, arguments: Dict[str, Any]) -> None:
self.name = name
super().__init__("variable", (QuotedString(name),), arguments)
def base_ref(self):
ref = super().base_ref()
return ref.replace("variable", "var")
class Locals(Block):
def __init__(self, arguments: Dict[str, Any]) -> None:
name = "locals"
super().__init__(name, (), arguments)
class Data(Block):
def __init__(
self,
name: str,
block_type: str,
arguments: Optional[Dict[str, Any]] = None
) -> None:
self.name = name
self.block_type = block_type
label = (QuotedString(block_type), QuotedString(name))
super().__init__("data", label, arguments)
class Resource(Block):
def __init__(self, name: str, res_type: str, arguments: Dict[str, Any] = None) -> None:
self.name = name
self.res_type = res_type
label = (QuotedString(res_type), QuotedString(name))
super().__init__("resource", label, arguments)
class Output(Block):
def __init__(self, name: str, arguments: Dict[str, Any]) -> None:
self.name = name
super().__init__("output", (QuotedString(self.name),), arguments)