Source code for ecs_composex.appmesh.appmesh_mesh

# SPDX-License-Identifier: MPL-2.0
# Copyright 2020-2022 John Mille <john@compose-x.io>

"""
Main module for AppMesh.

Once all services have been deployed and their VirtualNodes are setup, we deploy the Mesh for it.
"""
from __future__ import annotations

from typing import TYPE_CHECKING

import ecs_composex.common.troposphere_tools

if TYPE_CHECKING:
    from ecs_composex.common.settings import ComposeXSettings

from compose_x_common.compose_x_common import keyisset, set_else_none
from troposphere import AWS_ACCOUNT_ID, AWS_STACK_NAME, GetAtt, Output, Ref, appmesh

from ecs_composex.appmesh import appmesh_conditions, appmesh_params, metadata
from ecs_composex.appmesh.appmesh_aws import lookup_mesh_by_name
from ecs_composex.appmesh.appmesh_conditions import add_appmesh_conditions
from ecs_composex.appmesh.appmesh_node import MeshNode
from ecs_composex.appmesh.appmesh_params import MESH_NAME, MESH_OWNER_ID
from ecs_composex.appmesh.appmesh_router import MeshRouter
from ecs_composex.appmesh.appmesh_service import MeshService
from ecs_composex.common.cfn_params import ROOT_STACK_NAME
from ecs_composex.common.logging import LOG
from ecs_composex.common.stacks import ComposeXStack
from ecs_composex.common.troposphere_tools import (
    add_outputs,
    add_resource,
    build_template,
)
from ecs_composex.resources_import import import_record_properties


[docs]class Mesh: """ Class for AppMesh mesh """ mesh_title = "ServiceMesh" nodes_key = appmesh_params.NODES_KEY routers_key = appmesh_params.ROUTERS_KEY services_key = appmesh_params.SERVICES_KEY required_keys = [nodes_key, routers_key, services_key] def __init__( self, mesh_definition: dict, root_stack: ComposeXStack, settings: ComposeXSettings, ): """ Method to initialize the Mesh :param root_stack: The services root stack :type root_stack: ecs_composex.ecs.ServicesStack """ self.nodes = {} self.routers = {} self.services = {} self.routes = [] self.mesh_settings = mesh_definition["Settings"] self.mesh_properties = set_else_none( "Properties", mesh_definition, alt_value={} ) self.lookup = set_else_none("Lookup", mesh_definition) stack_parameters = {MESH_NAME.title: Ref(ROOT_STACK_NAME)} template = build_template( "AppMesh", [appmesh_params.MESH_OWNER_ID, appmesh_params.MESH_NAME] ) self.stack = ComposeXStack( "appmesh", stack_template=template, stack_parameters=stack_parameters ) self.appmesh = Ref(MESH_NAME) add_resource(settings.root_stack.stack_template, self.stack) appmesh_conditions.add_appmesh_conditions(self.stack.stack_template) if self.lookup: mesh_info = lookup_mesh_by_name( session=settings.session, mesh_name=self.lookup["MeshName"], mesh_owner=str(set_else_none("MeshOwner", self.lookup)), ) else: mesh_info = None if mesh_info: self.stack.Parameters.update( { appmesh_params.MESH_NAME_T: mesh_info[MESH_NAME.title], appmesh_params.MESH_OWNER_ID_T: mesh_info[MESH_OWNER_ID.title], } ) mesh_outputs = [Output(MESH_NAME.title, Value=Ref(MESH_NAME))] else: if self.mesh_properties: props = import_record_properties(self.mesh_properties, appmesh.Mesh) props["Metadata"] = metadata props["MeshName"] = Ref(MESH_NAME) else: props = { "Spec": appmesh.MeshSpec( EgressFilter=appmesh.EgressFilter(Type="ALLOW_ALL") ), "MeshName": Ref(MESH_NAME), } self.appmesh = appmesh.Mesh( self.mesh_title, **props, ) self.stack.stack_template.add_resource(self.appmesh) self.stack.Parameters.update( { appmesh_params.MESH_NAME_T: Ref(AWS_STACK_NAME), appmesh_params.MESH_OWNER_ID_T: Ref(AWS_ACCOUNT_ID), } ) if self.mesh_properties and keyisset("MeshName", self.mesh_properties): self.stack.Parameters.update( {MESH_NAME.title: self.mesh_properties["MeshName"]} ) mesh_outputs = [ Output(MESH_NAME.title, Value=GetAtt(self.appmesh, "MeshName")) ] add_outputs(self.stack.stack_template, mesh_outputs) for key in self.required_keys: if key not in self.mesh_settings.keys(): raise KeyError(f"Key {key} is missing. Required {self.required_keys}") self.define_nodes(settings) self.define_routes_and_routers() self.define_virtual_services(settings)
[docs] def define_nodes(self, settings: ComposeXSettings) -> None: """ Method to compile the nodes for the Mesh. """ nodes_keys = [ appmesh_params.NAME_KEY, appmesh_params.PROTOCOL_KEY, appmesh_params.BACKENDS_KEY, "Port", ] for node in self.mesh_settings[self.nodes_key]: if not set(node.keys()).issubset(nodes_keys): raise AttributeError( f"Nodes must have set {nodes_keys}. Got", node.keys() ) service_families = [ settings.families[name] for name in settings.families if settings.families[name].name == node[appmesh_params.NAME_KEY] ] if len(service_families) > 1: raise LookupError( "More than one family matched for the node.", service_families, node[appmesh_params.NAME_KEY], ) elif not service_families: raise LookupError( "No family could be matched for the given node", settings.families, node[appmesh_params.NAME_KEY], ) LOG.debug(node) family = service_families[0] mesh_node = MeshNode( family, node[appmesh_params.PROTOCOL_KEY], node["Port"], self, settings, ( node[appmesh_params.BACKENDS_KEY] if keyisset(appmesh_params.BACKENDS_KEY, node) else None ), ) self.nodes[family.logical_name] = mesh_node self.nodes[family.logical_name].get_node_param = GetAtt( mesh_node.node, "VirtualNodeName" ) self.nodes[family.logical_name].get_sg_param = GetAtt( self.nodes[family.logical_name].param_name, f"Outputs.{family.logical_name}GroupId", )
[docs] def define_routes_and_routers(self): """ Method to register routers """ for router in self.mesh_settings[self.routers_key]: name = router[appmesh_params.NAME_KEY] self.routers[name] = MeshRouter( router[appmesh_params.NAME_KEY], router, self, self.nodes, )
[docs] def define_virtual_services(self, settings): """ Method to parse the services and map them to nodes and routers. """ for service in self.mesh_settings[self.services_key]: name = service[appmesh_params.NAME_KEY] self.services[name] = MeshService( name, service, self.routers, self.nodes, self, settings, )
[docs] def render_mesh_template(self, stack: ComposeXStack, settings: ComposeXSettings): """ Method to create the AppMesh template stack. :param ComposeXStack stack: The services root stack """ if ( isinstance(self.appmesh, Mesh) and self.appmesh.title not in stack.stack_template.resources ): stack.stack_template.add_resource(self.appmesh) add_appmesh_conditions(stack.stack_template) self.process_mesh_components(stack, settings)
[docs] def process_mesh_components(self, services_stack, settings): for router_name in self.routers: router = self.routers[router_name] services_stack.stack_template.add_resource(router.router) for route in router.routes: services_stack.stack_template.add_resource(route) for service_name in self.services: service = self.services[service_name] services_stack.stack_template.add_resource(service.service) service.add_dns_entries(self.stack, settings) for res_name in services_stack.stack_template.resources: res = services_stack.stack_template.resources[res_name] if issubclass(type(res), ComposeXStack) and isinstance(self.appmesh, Mesh): res.add_dependencies(self.appmesh.title) for node_name in self.nodes: if self.nodes[node_name].backends: self.nodes[node_name].expand_backends( self, settings.root_stack, self.services )