433 lines
12 KiB
Python
433 lines
12 KiB
Python
""" terrascript/__init__.py
|
|
|
|
Base classes and functions that are used everywhere else in this project.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
|
|
__author__ = "Markus Juenemann <markus@juenemann.net>"
|
|
__version__ = "0.10.0"
|
|
__license__ = 'BSD 2-clause "Simplified" License'
|
|
|
|
"""JSON indentation level."""
|
|
INDENT = 2
|
|
|
|
"""Set to enable some debugging."""
|
|
DEBUG = False
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
PROVIDER_KEY = "provider"
|
|
RESOURCE_KEY = "resource"
|
|
MODULE_KEY = "module"
|
|
VARIABLE_KEY = "variable"
|
|
OUTPUT_KEY = "output"
|
|
LOCALS_KEY = "locals"
|
|
DATA_KEY = "data"
|
|
TERRAFORM_KEY = "terraform"
|
|
|
|
|
|
class Attribute(str):
|
|
"""An `Attribute` handles access to not yet known attributes.
|
|
|
|
This called by `Block.__getattr__` to deal with
|
|
|
|
In the example below the ``aws_instance`` does not have attributes
|
|
``.server`` and in turn ``.server.private_ip``. To prevent Python
|
|
from raising an `AttributeError` the `Attribute.__getattr__()` method
|
|
creates a new string by appending the attribute name.
|
|
|
|
Python:
|
|
|
|
config = terrascript.Terrascript()
|
|
config += terrascript.aws.aws(version='~> 2.0', region='us-east-1')
|
|
aws_instance = terrascript.aws.r.aws_instance('web', ...)
|
|
config += aws_instance
|
|
config += terrascript.Output('instance_ip_addr',
|
|
value=aws_instance.server.private_ip)
|
|
^^^^^^^^^^^^^^^^^^
|
|
|
|
JSON:
|
|
"""
|
|
|
|
def __getattr__(self, name):
|
|
return Attribute(f"{self}.{name}")
|
|
|
|
|
|
class Block(dict):
|
|
""" A `Block` is a dictionary-like container for other content. """
|
|
|
|
def __init__(self, **kwargs):
|
|
# Convert variables into references instead of adding the actual dict.
|
|
for k, v in kwargs.items():
|
|
if isinstance(v, Variable):
|
|
kwargs[k] = str(v)
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
def __getattr__(self, attr):
|
|
"""Special handling for accessing attributes,
|
|
|
|
If ``Block.attr`` does not exist, try to return Block[attr]. If that
|
|
does not exists either, return `attr` as a string, prefixed
|
|
by the name (and type) of the Block that is referenced.
|
|
|
|
This is for example necessary for referencing an attribute of a
|
|
Terraform resource which only becomes available after the resource
|
|
has been created.
|
|
|
|
Example:
|
|
|
|
instance = terrascript.resources.aws_instance("server", ...)
|
|
output = terrascript.Output("instance_ip_addr",
|
|
value=instance.private_ip)
|
|
^^^^^^^^^^
|
|
Where ``instance.private_ip`` does not (yet) exist.
|
|
"""
|
|
# Try to return the entry in the dictionary. Otherwise return a string
|
|
# which must be formatted differently depending on what is referenced.
|
|
if attr in self:
|
|
return self[attr]
|
|
elif attr.startswith("__"):
|
|
raise AttributeError
|
|
else:
|
|
if isinstance(self, Resource):
|
|
return Attribute(f"{self.__class__.__name__}.{self._name}.{attr}")
|
|
if isinstance(self, Module):
|
|
return Attribute(f"module.{self._name}.{attr}")
|
|
if isinstance(self, Variable):
|
|
return Attribute(f"var.{self._name}.{attr}")
|
|
elif isinstance(self, Locals):
|
|
return Attribute(f"local.{attr}")
|
|
elif isinstance(self, Data):
|
|
# data.google_compute_image.NAME.ATTR
|
|
return Attribute(f"data.{self.__class__.__name__}.{self._name}.{attr}")
|
|
else:
|
|
raise AttributeError(attr)
|
|
|
|
|
|
class NamedBlock(Block):
|
|
def __init__(self, _name, **kwargs):
|
|
self._name = _name
|
|
super().__init__(**kwargs)
|
|
# TODO: Can this be done similar to NamedSubBlock?
|
|
|
|
|
|
class NamedSubBlock(Block):
|
|
"""NamedSubBlocks are similar to NamedBlocks except that the `name`
|
|
is the key to a nested dictionary contain `kwargs`. NamedSubBlocks
|
|
are not added to the top-level Terrascript structure but are
|
|
arguments to another block. The backend argument to a Terraform
|
|
block is a good example.
|
|
|
|
:param name: Name of the block, e.g. 'consul'.
|
|
"""
|
|
|
|
def __init__(self, name, **kwargs):
|
|
super().__init__()
|
|
self[name] = kwargs
|
|
|
|
|
|
class Terrascript(dict):
|
|
"""Top-level container for Terraform configurations.
|
|
|
|
:param *objects: Optional list of Terrascript data sources, resources,
|
|
"""
|
|
|
|
def __init__(self, *objects):
|
|
super().__init__()
|
|
|
|
for item in objects:
|
|
self.add(item)
|
|
|
|
def __str__(self):
|
|
return json.dumps(self, indent=INDENT)
|
|
|
|
def __add__(self, block):
|
|
""" Add a block to the configuration using the ``+`` syntax. """
|
|
#
|
|
# Resource
|
|
#
|
|
if isinstance(block, Resource):
|
|
# self['resource']
|
|
if RESOURCE_KEY not in self:
|
|
self[RESOURCE_KEY] = {}
|
|
# self['resource'][RESOURCE]
|
|
if block.__class__.__name__ not in self[RESOURCE_KEY]:
|
|
self[RESOURCE_KEY][block.__class__.__name__] = {}
|
|
# self['resource'][RESOURCE][NAME] = {...}
|
|
self[RESOURCE_KEY][block.__class__.__name__][block._name] = block
|
|
#
|
|
# Data
|
|
#
|
|
elif isinstance(block, Data):
|
|
# self['data']
|
|
if DATA_KEY not in self:
|
|
self[DATA_KEY] = {}
|
|
# self['data'][DATASOURCE]
|
|
if block.__class__.__name__ not in self[DATA_KEY]:
|
|
self[DATA_KEY][block.__class__.__name__] = {}
|
|
# self['data'][DATASOURCE][NAME] = {...}
|
|
self[DATA_KEY][block.__class__.__name__][block._name] = block
|
|
#
|
|
# Module
|
|
#
|
|
elif isinstance(block, Module):
|
|
# self['module']
|
|
if MODULE_KEY not in self:
|
|
self[MODULE_KEY] = {}
|
|
# self['module'][NAME] = {}
|
|
self[MODULE_KEY][block._name] = block
|
|
#
|
|
# Provider
|
|
#
|
|
elif isinstance(block, Provider):
|
|
# self['provider']
|
|
if PROVIDER_KEY not in self:
|
|
self[PROVIDER_KEY] = {}
|
|
# self['provider'][PROVIDER]
|
|
if block.__class__.__name__ not in self[PROVIDER_KEY]:
|
|
self[PROVIDER_KEY][block.__class__.__name__] = []
|
|
# self['provider'][PROVIDER] = [{...}, ...]
|
|
self[PROVIDER_KEY][block.__class__.__name__].append(block)
|
|
#
|
|
# Variable
|
|
#
|
|
elif isinstance(block, Variable):
|
|
# self['variable']
|
|
if VARIABLE_KEY not in self:
|
|
self[VARIABLE_KEY] = Block()
|
|
# self['variable'][NAME]
|
|
self[VARIABLE_KEY][block._name] = block
|
|
#
|
|
# Output
|
|
#
|
|
elif isinstance(block, Output):
|
|
# self['output']
|
|
if OUTPUT_KEY not in self:
|
|
self[OUTPUT_KEY] = Block()
|
|
# self['output'][NAME]
|
|
self[OUTPUT_KEY][block._name] = block
|
|
#
|
|
# Locals
|
|
#
|
|
elif isinstance(block, Locals):
|
|
# self['locals']
|
|
if LOCALS_KEY not in self:
|
|
self[LOCALS_KEY] = Block()
|
|
# self['locals'][NAME]
|
|
self[LOCALS_KEY].update(block)
|
|
#
|
|
# Terraform
|
|
#
|
|
elif isinstance(block, Terraform):
|
|
# self['terraform']
|
|
if TERRAFORM_KEY not in self:
|
|
self[TERRAFORM_KEY] = {}
|
|
# self['terraform'] = {{...}, ...}
|
|
self[TERRAFORM_KEY] = self.merge(self[TERRAFORM_KEY], block)
|
|
#
|
|
# else
|
|
#
|
|
else:
|
|
raise TypeError(
|
|
"A {} cannot be added to the configuration".format(
|
|
block.__class__.__name__
|
|
)
|
|
)
|
|
|
|
return self
|
|
|
|
def add(self, item):
|
|
""" Add to the configuration using the ``+`` syntax. """
|
|
self.__add__(item)
|
|
return item # for backwards compatibility!
|
|
|
|
def update(self, other, **kwargs):
|
|
del kwargs
|
|
for o in other:
|
|
self.add(o)
|
|
|
|
def merge(self, base, additions):
|
|
"""Recursively merge additions into base and return the updated result
|
|
|
|
:param base: dict to use as base for merge
|
|
:param additions: dict to merge values from
|
|
:return:
|
|
"""
|
|
if not isinstance(base, dict) or not isinstance(additions, dict):
|
|
return additions
|
|
|
|
for key in additions:
|
|
if key in base:
|
|
base[key] = self.merge(base[key], additions[key])
|
|
else:
|
|
base[key] = additions[key]
|
|
|
|
return base
|
|
|
|
def __iter__(self):
|
|
""" Iterate over top-level items. """
|
|
|
|
def recurse(res):
|
|
if isinstance(res, (Resource, Data, Provider, Variable, Module, Output)):
|
|
yield res
|
|
elif isinstance(res, dict):
|
|
for k, v in res.items():
|
|
yield from recurse(v)
|
|
elif isinstance(res, list):
|
|
for i in res:
|
|
yield from recurse(i)
|
|
|
|
yield from recurse(self)
|
|
|
|
|
|
# Top-level blocks ----------------------------------------
|
|
|
|
|
|
class Resource(NamedBlock):
|
|
""" Terraform resource block. """
|
|
|
|
pass
|
|
|
|
|
|
class Data(NamedBlock):
|
|
""" Terraform data source block. """
|
|
|
|
pass
|
|
|
|
|
|
class Provider(Block):
|
|
"""Terraform provider
|
|
|
|
HCL:
|
|
|
|
provider "aws" {
|
|
region = "us-east-1"
|
|
version = "u~> 2.0"
|
|
}
|
|
|
|
JSON:
|
|
|
|
"provider": {
|
|
"aws": [
|
|
{
|
|
"region": "us-east-1",
|
|
"version": "~> 2.0"
|
|
}
|
|
]
|
|
}
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
## Populate `version` and `source` arguments from info provided
|
|
## by the actual provider implementation.
|
|
##
|
|
#if "version" not in kwargs:
|
|
# kwargs["version"] = self.__version__
|
|
#if "source" not in kwargs:
|
|
# kwargs["source"] = self.__source__
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
|
class Variable(NamedBlock):
|
|
def __repr__(self):
|
|
return f"${{var.{self._name}}}"
|
|
|
|
|
|
class Module(NamedBlock):
|
|
"""Terraform child module call.
|
|
|
|
https://www.terraform.io/docs/configuration/modules.html
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
class Output(NamedBlock):
|
|
pass
|
|
|
|
|
|
# Other blocks ----------------------------------------
|
|
|
|
|
|
class Provisioner(dict):
|
|
"""A provisioner is a nested dictionary.
|
|
|
|
The `name` argument must be a valid Terraform provisioner.
|
|
|
|
>>> import terrascript
|
|
>>> p = terrascript.Provisioner("local-exec", command="echo 'Hello World'")
|
|
>>> print(p)
|
|
{'local-exec': {'command': "echo 'Hello World"}}
|
|
|
|
https://www.terraform.io/docs/provisioners/index.html
|
|
"""
|
|
|
|
def __init__(self, name, **kwargs):
|
|
super().__init__()
|
|
self[name] = kwargs
|
|
|
|
|
|
class Connection(Block):
|
|
pass
|
|
|
|
|
|
class Backend(NamedSubBlock):
|
|
pass
|
|
|
|
|
|
class Terraform(Block):
|
|
pass
|
|
|
|
|
|
class Locals(Block):
|
|
# Not working, use Python variables instead.
|
|
pass
|
|
|
|
|
|
class Function(Block):
|
|
pass
|
|
|
|
|
|
# Lower case classes for backwards compatibility with the exception of 'locals'
|
|
# and the classes that are subclassed in modules (resource, data, provider).
|
|
variable = Variable
|
|
module = Module
|
|
output = Output
|
|
provisioner = Provisioner
|
|
connection = Connection
|
|
backend = Backend
|
|
terraform = Terraform
|
|
function = Function
|
|
|
|
__all__ = [
|
|
"Backend",
|
|
"backend",
|
|
"Block",
|
|
"Connection",
|
|
"connection",
|
|
"Data",
|
|
"data",
|
|
"Function",
|
|
"function",
|
|
"Locals",
|
|
"Module",
|
|
"module",
|
|
"Output",
|
|
"output",
|
|
"Provider",
|
|
"provider",
|
|
"Provisioner",
|
|
"provisioner",
|
|
"Resource",
|
|
"resource",
|
|
"Terraform",
|
|
"terraform",
|
|
"Terrascript",
|
|
"Variable",
|
|
"variable",
|
|
]
|