# -*- coding: utf-8 -*-
"""
Canned Template implements the parameterized template creation logic. It allows
developer to add more custom logic before or after the template creation.
"""
try:
from typing import Type, Dict, Union
except:
pass
import os
from collections import OrderedDict
from configirl import Constant, Derivable, ConfigClass
from troposphere import Ref, Sub
from .mate import Template
def slugify(name):
return name.replace("_", "-")
def camelcase(name):
return "".join([
word[0].upper() + word[1:].lower()
for word in slugify(name).split("-")
])
[docs]def helper_fn_sub(text, *params):
"""
A helper function to construct FnSub snippet easily. It uses python
str.format place holder ``{}``, it will be replaced with parameters
substitution. For example, ``helper_fn_sub("{}-my-ec2-instance", param_env_name)``
will be translate to this. The ``param_env_name`` is the EnvName template
parameter::
{
"Fn::Sub": [
"${EnvName}-my-ec2-instance"
{
"EnvName": {
"Ref": {
"EnvName"
}
}
}
]
}
"""
return Sub(
text.format(*["${" + param.title + "}" for param in params]),
{
param.title: Ref(param)
for param in params
}
)
[docs]class Canned(ConfigClass):
"""
Represent a Canned CloudFormation Template. Defines the creation logic and
parameters of a Template. It is a Factory Class for Template.
:type logic_id: str
:param logic_id: logic id of this template been used in the master
template as a nested template
:type root_dir: str
:param root_dir: dir path of where the template file been created
:type rel_path: str
:param metadata: relative path of where the template file been created
**中文文档**
Canned 的英文是 罐装的. 这里是表示, 此类是一个 Template 的罐头, 类本身实际是一个
``configirl.ConfigClass`` 的子类, 能对配置数据进行管理. 而通过
``def create_template()`` 方法定义了基于配置数据创建 ``troposphere_mate.Template``
实例的逻辑. 也就是说, 创建 Canned 实例时, 只读取配置数据, 并没有真正创建 Template 实例.
此类实现了对 Template 的复用. 比如你定义了一个 VPC Tier 的 Template, 里面包含
VPC, Subnet 等资源, 而你可以通过更新配置数据的方式, 复用该模板.
之所以在 Template 之外进行一层包装, 是因为在创建 Template 的实例过程中需要执行必要的
validation. 而我们定义可复用模板时, 必然所有的 Resources Properties 都是参数化的,
换言之, 定义时还未可知. 所以我们要将配置参数和创建模板分离开.
"""
_create_template_called_counter = 0
logic_id = None # type: str
root_dir = None # type: str
rel_path = None # type: str
def __init__(self,
metadata=None,
**kwargs):
"""
:type metadata: OrderedDict
:param metadata: arbitrary metadata
"""
super(Canned, self).__init__(**kwargs)
self.template = None # type: Template
self.metadata = OrderedDict() if metadata is None else metadata
@property
def abspath(self):
return os.path.join(self.root_dir, self.rel_path)
def to_file(self, json_or_yml="json", overwrite=False, **kwargs):
if self.template is None:
raise Exception("Before dumping to template file, Canned Template "
"has to call Canned.create_template() method first!")
try:
abspath = self.abspath
except:
raise ValueError("you have to specify `Canned.root_dir` and `Canned.rel_path` "
"first to derive the template path.")
if os.path.exists(abspath):
if overwrite is False:
raise EnvironmentError("%s already exists! You can use "
".to_file(..., overwrite=True) to enable overwrite.")
self.template.to_file(abspath, json_or_yml=json_or_yml, **kwargs)
def create_template(self, **kwargs):
if self._create_template_called_counter != 0:
raise Warning(
".create_template() method should be only call once!")
self._create_template_called_counter += 1
self.pre_create_template_hooker(**kwargs)
self.do_create_template(**kwargs)
if not isinstance(self.template, Template):
raise TypeError("Canned.do_create_template() method has to return a {}".format(
Template.__name__))
self.template.set_version()
self.post_create_template_hooker(**kwargs)
return self.template
def pre_create_template_hooker(self, **kwargs):
pass
def do_create_template(self, **kwargs):
raise NotImplementedError
def post_create_template_hooker(self, **kwargs):
pass
[docs]class MultiEnvBasicConfig(Canned):
"""
A multi environment / stage config settings.
**中文文档**
一个常用的 Multi Stage / Environment 的配置模板.
"""
PROJECT_NAME = Constant()
PROJECT_NAME_SLUG = Derivable()
@PROJECT_NAME_SLUG.getter
def get_PROJECT_NAME_SLUG(self):
return self.PROJECT_NAME.get_value().replace("_", "-")
STAGE = Constant()
ENVIRONMENT_NAME = Derivable()
@ENVIRONMENT_NAME.getter
def get_ENVIRONMENT_NAME(self):
return "{}-{}".format(self.PROJECT_NAME_SLUG.get_value(), self.STAGE.get_value())
STACK_NAME = Derivable()
@STACK_NAME.getter
def get_STACK_NAME(self):
return self.ENVIRONMENT_NAME.get_value()
ENV_TAG = Derivable() # the environment tag for orchestration
@ENV_TAG.getter
def get_ENV_TAG(self):
return self.STAGE.get_value()
COMMON_TAGS = Derivable()
@COMMON_TAGS.getter
def get_COMMON_TAGS(self):
return dict(
Name=self.ENVIRONMENT_NAME.get_value(),
Project=self.PROJECT_NAME_SLUG.get_value(),
Stage=self.STAGE.get_value(),
EnvName=self.ENVIRONMENT_NAME.get_value(),
)
[docs] def post_create_template_hooker(self, **kwargs):
"""
Automatically update common tags after the template been created.
"""
self.template.update_tags(
self.COMMON_TAGS.get_value(), overwrite=False)
[docs]class ServerlessConfig(MultiEnvBasicConfig): # pragma: no cover
"""
Serverless application config settings for common fields.
"""
AWS_PROFILE_FOR_DEPLOY = Constant()
AWS_PROFILE_FOR_BOTO3 = Derivable()
@AWS_PROFILE_FOR_BOTO3.getter
def get_AWS_PROFILE_FOR_BOTO3(self):
if self.is_aws_lambda_runtime():
return None
elif self.is_aws_ec2_runtime():
return None
elif self.is_ci_runtime():
return None
else:
return self.AWS_PROFILE_FOR_DEPLOY.get_value()
S3_BUCKET_FOR_DEPLOY = Constant()
S3_PREFIX_LAMBDA_ARTIFACT = Constant()
"""
usually it is ``lambda/{github_account_username}/{github_repo_name}``.
The final source code s3 key is
``lambda/{github_account_username}/{github_repo_name}/{version}/source.zip``
run ``make lbd-info`` command to get more information
"""
LAMBDA_LAYER_ARNS = Constant()
LAMBDA_LATEST_LAYER_ARNS = Derivable(dont_dump=True)
"""
Latest lambda layer arn for ``awslambda.Function.Layers`` property
"""
@LAMBDA_LATEST_LAYER_ARNS.getter
def get_LAMBDA_LATEST_LAYER_ARNS(self):
import boto3
boto_ses = boto3.session.Session(profile_name=self.AWS_PROFILE_FOR_BOTO3.get_value())
lbd_client = boto_ses.client("lambda")
latest_layer_arn = None
list_layers_response = lbd_client.list_layers()
for layer_data in list_layers_response["Layers"]:
if layer_data["LayerName"] == self.PROJECT_NAME_SLUG.get_value():
latest_layer_arn = layer_data["LatestMatchingVersion"]["LayerVersionArn"]
if latest_layer_arn is None:
raise ValueError
return [
latest_layer_arn,
]
API_GATEWAY_REST_API_NAME = Derivable()
@API_GATEWAY_REST_API_NAME.getter
def get_API_GATEWAY_REST_API_NAME(self):
return self.ENVIRONMENT_NAME.get_value()
API_GATEWAY_REST_API_DEPLOYMENT_STAGE_NAME = Derivable()
@API_GATEWAY_REST_API_DEPLOYMENT_STAGE_NAME.getter
def get_API_GATEWAY_REST_API_DEPLOYMENT_STAGE_NAME(self):
return self.STAGE.get_value()
S3_URI_ATHENA_RESULT = Derivable()
@S3_URI_ATHENA_RESULT.getter
def get_S3_URI_ATHENA_RESULT(self):
return "s3://{}/athena/result".format(self.S3_BUCKET_FOR_DEPLOY.get_value())
ATHENA_DATABASE_NAME = Derivable()
@ATHENA_DATABASE_NAME.getter
def get_ATHENA_DATABASE_NAME(self):
return self.ENVIRONMENT_NAME.get_value().replace("-", "_")
LABELS_TO_DEPLOY = Constant()
LABELS_TO_IGNORE = Constant()