# SPDX-License-Identifier: MPL-2.0
# Copyright 2020-2022 John Mille <john@compose-x.io>
"""
Module to define CloudWatch alarms in x-alarms and match these to services
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ecs_composex.common.settings import ComposeXSettings
from compose_x_common.compose_x_common import keyisset
from troposphere import GetAtt, Output, Ref
from troposphere.cloudwatch import Alarm as CWAlarm
from troposphere.cloudwatch import CompositeAlarm, MetricDimension
from ecs_composex.alarms.alarms_helpers import create_alarms
from ecs_composex.alarms.alarms_stack import Alarm
from ecs_composex.common.cfn_params import Parameter
from ecs_composex.common.logging import LOG
from ecs_composex.common.troposphere_tools import (
add_outputs,
add_parameters,
add_update_mapping,
)
from ecs_composex.ecs.ecs_params import CLUSTER_NAME, SERVICE_SCALING_TARGET
from ecs_composex.ecs.service_scaling.helpers import (
generate_alarm_scaling_out_policy,
reset_to_zero_policy,
)
from ecs_composex.sns.sns_params import RES_KEY as SNS_KEY
from ecs_composex.sns.sns_params import TOPIC_ARN, TOPIC_ARN_RE
from ecs_composex.sns.sns_stack import Topic
[docs]def get_alarm_actions(alarm):
"""
Function to get the alarm actions
:param alarm:
:return: the okay and alarm actions
:rtype: tuple
"""
if hasattr(alarm.cfn_resource, "OKActions"):
okay_actions = getattr(alarm.cfn_resource, "OKActions")
else:
okay_actions = []
setattr(alarm.cfn_resource, "OKActions", okay_actions)
if hasattr(alarm.cfn_resource, "AlarmActions"):
alarm_actions = getattr(alarm.cfn_resource, "AlarmActions")
else:
alarm_actions = []
setattr(alarm.cfn_resource, "AlarmActions", alarm_actions)
return okay_actions, alarm_actions
[docs]def add_service_actions(
alarm, alarms_stack, target, scaling_in_policy, scaling_out_policy
):
"""
Function to update the alarm properties with OKActions and AlarmActions
:param ecs_composex.alarms.alarms_stack.Alarm alarm:
:param ecs_composex.common.stacks.ComposeXStack alarms_stack:
:param tuple target:
:param scaling_in_policy:
:param scaling_out_policy:
"""
setattr(
alarm,
"Threshold",
float(
scaling_out_policy.StepScalingPolicyConfiguration.StepAdjustments[
0
].MetricIntervalLowerBound
),
)
if not alarm.cfn_resource:
raise AttributeError(f"Alarm {alarm.logical_name} has no CFN object associated")
service_scaling_in_policy_param = Parameter(
f"{target[0].logical_name}ScaleInPolicy", Type="String"
)
service_scaling_out_policy_param = Parameter(
f"{target[0].logical_name}ScaleOutPolicy", Type="String"
)
add_parameters(
alarms_stack.stack_template,
[service_scaling_in_policy_param, service_scaling_out_policy_param],
)
add_outputs(
target[0].template,
[
Output(
f"{target[0].logical_name}ScaleInPolicy",
Value=Ref(scaling_in_policy),
),
Output(
f"{target[0].logical_name}ScaleOutPolicy",
Value=Ref(scaling_out_policy),
),
],
)
alarms_stack.Parameters.update(
{
service_scaling_in_policy_param.title: GetAtt(
target[0].logical_name,
f"Outputs.{target[0].logical_name}ScaleInPolicy",
),
service_scaling_out_policy_param.title: GetAtt(
target[0].logical_name,
f"Outputs.{target[0].logical_name}ScaleOutPolicy",
),
}
)
actions = get_alarm_actions(alarm)
actions[0].append(Ref(service_scaling_in_policy_param))
actions[1].append(Ref(service_scaling_out_policy_param))
[docs]def handle_service_scaling(alarm, alarms_stack):
"""
Function to create the scaling steps for defined services
:param ecs_composex.alarms.alarms_stack.Alarm alarm:
:param ecs_composex.common.stacks.ComposeXStack alarms_stack:
"""
for target in alarm.families_scaling:
if SERVICE_SCALING_TARGET not in target[0].template.resources:
LOG.warning(
f"No Scalable target defined for {target[0].name}."
" You need to define `scaling.scaling_range` in x-configs first. No scaling applied"
)
return
scaling_out_policy = generate_alarm_scaling_out_policy(
target[0].logical_name,
target[0].template,
target[1],
scaling_source=alarm.logical_name,
)
scaling_in_policy = reset_to_zero_policy(
target[0].logical_name,
target[0].template,
target[1],
scaling_source=alarm.logical_name,
)
add_service_actions(
alarm, alarms_stack, target, scaling_in_policy, scaling_out_policy
)
[docs]def map_topic_to_action(alarm, notify_on, topic_identifier):
"""
Function to map the topic to specific actions
:param alarm: alarm props for alarms
:param str notify_on:
:param topic_identifier:
:return:
"""
actions = get_alarm_actions(alarm)
if notify_on == "all":
actions[0].append(topic_identifier)
actions[1].append(topic_identifier)
elif notify_on == "alarm":
actions[1].append(topic_identifier)
elif notify_on == "okay":
actions[0].append(topic_identifier)
[docs]def handle_notify_on(topic_def):
"""
Function to validate parameter NotifyOn
:param dict topic_def:
:return:
"""
valid_values = ["all", "okay", "alarm"]
notify_on = "alarm"
if not keyisset("NotifyOn", topic_def):
LOG.warning("NotifyOn was not set for topic. Will default to AlarmActions")
else:
if not isinstance(topic_def["NotifyOn"], str):
raise TypeError("NotifyOn must be a string")
notify_on = topic_def["NotifyOn"].lower()
if notify_on not in valid_values:
raise ValueError(
"The value for NotifyOn",
notify_on,
"Is not valid. Expected one of",
valid_values,
)
return notify_on
[docs]def handle_compose_topics(alarm, alarms_stack, settings, topic_def, notify_on):
"""
Function to handle x-alarms to x-sns topics
:param ecs_composex.alarms.alarms_stack.Alarm alarm:
:param ecs_composex.common.stacks.ComposeXStack alarms_stack:
:param ecs_composex.common.settings.ComposeXSettings settings:
:param dict topic_def:
:param str notify_on:
:return:
"""
topic = settings.find_resource(f"x-sns::{topic_def['x-sns']}")
if topic and not topic.attributes_outputs:
topic.init_outputs()
topic.generate_outputs()
topic_arn = topic.attributes_outputs[TOPIC_ARN]
if topic.cfn_resource:
add_parameters(alarms_stack.stack_template, [topic_arn["ImportParameter"]])
alarms_stack.Parameters.update({topic_arn["Name"]: topic_arn["ImportValue"]})
map_topic_to_action(alarm, notify_on, Ref(topic_arn["ImportParameter"]))
else:
add_update_mapping(
alarms_stack.stack_template,
topic.module.mapping_key,
settings.mappings[topic.module.mapping_key],
)
map_topic_to_action(alarm, notify_on, topic_arn["ImportValue"])
[docs]def handle_alarm_topics(alarm, alarms_stack, settings):
"""
Function to add the topics actions for defined topics
:param ecs_composex.alarms.alarms_stack.Alarm alarm:
:param ecs_composex.common.stacks.ComposeXStack alarms_stack:
:param ecs_composex.common.settings.ComposeXSettings settings:
:return:
"""
for topic in alarm.topics:
notify_on = handle_notify_on(topic)
if keyisset("TopicArn", topic):
if not TOPIC_ARN_RE.match(topic["TopicArn"]):
raise ValueError("Invalid ARN for topic", topic["TopicArn"])
map_topic_to_action(alarm, notify_on, topic["TopicArn"])
elif keyisset(SNS_KEY, topic):
if not keyisset(SNS_KEY, settings.compose_content) or (
keyisset(SNS_KEY, settings.compose_content)
and not keyisset(
topic[SNS_KEY],
settings.compose_content[SNS_KEY],
)
):
raise KeyError(
f"There is no topic {topic[SNS_KEY]} defined in {SNS_KEY}",
settings.compose_content[SNS_KEY].keys(),
)
handle_compose_topics(alarm, alarms_stack, settings, topic, notify_on)
[docs]def alarms_to_ecs(resources, services_stack, res_root_stack, settings):
new_resources = [resource for resource in resources.values() if not resource.lookup]
for alarm in new_resources:
if alarm.in_composite:
continue
handle_service_scaling(alarm, res_root_stack)
if alarm.topics:
handle_alarm_topics(alarm, res_root_stack, settings)