forked from mirrors/gecko-dev
195 lines
5.9 KiB
Python
195 lines
5.9 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
"""
|
|
Add notifications to tasks via Taskcluster's notify service.
|
|
|
|
See https://docs.taskcluster.net/docs/reference/core/notify/usage for
|
|
more information.
|
|
"""
|
|
from voluptuous import ALLOW_EXTRA, Any, Exclusive, Optional, Required
|
|
|
|
from taskgraph.transforms.base import TransformSequence
|
|
from taskgraph.util.schema import Schema, optionally_keyed_by, resolve_keyed_by
|
|
|
|
_status_type = Any(
|
|
"on-completed",
|
|
"on-defined",
|
|
"on-exception",
|
|
"on-failed",
|
|
"on-pending",
|
|
"on-resolved",
|
|
"on-running",
|
|
)
|
|
|
|
_recipients = [
|
|
{
|
|
Required("type"): "email",
|
|
Required("address"): optionally_keyed_by("project", "level", str),
|
|
Optional("status-type"): _status_type,
|
|
},
|
|
{
|
|
Required("type"): "matrix-room",
|
|
Required("room-id"): str,
|
|
Optional("status-type"): _status_type,
|
|
},
|
|
{
|
|
Required("type"): "pulse",
|
|
Required("routing-key"): str,
|
|
Optional("status-type"): _status_type,
|
|
},
|
|
{
|
|
Required("type"): "slack-channel",
|
|
Required("channel-id"): str,
|
|
Optional("status-type"): _status_type,
|
|
},
|
|
]
|
|
|
|
_route_keys = {
|
|
"email": "address",
|
|
"matrix-room": "room-id",
|
|
"pulse": "routing-key",
|
|
"slack-channel": "channel-id",
|
|
}
|
|
"""Map each type to its primary key that will be used in the route."""
|
|
|
|
NOTIFY_SCHEMA = Schema(
|
|
{
|
|
Exclusive("notify", "config"): {
|
|
Required("recipients"): [Any(*_recipients)],
|
|
Optional("content"): {
|
|
Optional("email"): {
|
|
Optional("subject"): str,
|
|
Optional("content"): str,
|
|
Optional("link"): {
|
|
Required("text"): str,
|
|
Required("href"): str,
|
|
},
|
|
},
|
|
Optional("matrix"): {
|
|
Optional("body"): str,
|
|
Optional("formatted-body"): str,
|
|
Optional("format"): str,
|
|
Optional("msg-type"): str,
|
|
},
|
|
Optional("slack"): {
|
|
Optional("text"): str,
|
|
Optional("blocks"): list,
|
|
Optional("attachments"): list,
|
|
},
|
|
},
|
|
},
|
|
# Continue supporting the legacy schema for backwards compat.
|
|
Exclusive("notifications", "config"): {
|
|
Required("emails"): optionally_keyed_by("project", "level", [str]),
|
|
Required("subject"): str,
|
|
Optional("message"): str,
|
|
Optional("status-types"): [_status_type],
|
|
},
|
|
},
|
|
extra=ALLOW_EXTRA,
|
|
)
|
|
"""Notify schema."""
|
|
|
|
transforms = TransformSequence()
|
|
transforms.add_validate(NOTIFY_SCHEMA)
|
|
|
|
|
|
def _convert_legacy(config, legacy, label):
|
|
"""Convert the legacy format to the new one."""
|
|
notify = {
|
|
"recipients": [],
|
|
"content": {"email": {"subject": legacy["subject"]}},
|
|
}
|
|
resolve_keyed_by(
|
|
legacy,
|
|
"emails",
|
|
label,
|
|
**{
|
|
"level": config.params["level"],
|
|
"project": config.params["project"],
|
|
},
|
|
)
|
|
|
|
status_types = legacy.get("status-types", ["on-completed"])
|
|
for email in legacy["emails"]:
|
|
for status_type in status_types:
|
|
notify["recipients"].append(
|
|
{"type": "email", "address": email, "status-type": status_type}
|
|
)
|
|
|
|
notify["content"]["email"]["content"] = legacy.get("message", legacy["subject"])
|
|
return notify
|
|
|
|
|
|
def _convert_content(content):
|
|
"""Convert the notify content to Taskcluster's format.
|
|
|
|
The Taskcluster notification format is described here:
|
|
https://docs.taskcluster.net/docs/reference/core/notify/usage
|
|
"""
|
|
tc = {}
|
|
if "email" in content:
|
|
tc["email"] = content.pop("email")
|
|
|
|
for key, obj in content.items():
|
|
for name in obj.keys():
|
|
tc_name = "".join(part.capitalize() for part in name.split("-"))
|
|
tc[f"{key}{tc_name}"] = obj[name]
|
|
return tc
|
|
|
|
|
|
@transforms.add
|
|
def add_notifications(config, tasks):
|
|
for task in tasks:
|
|
label = "{}-{}".format(config.kind, task["name"])
|
|
if "notifications" in task:
|
|
notify = _convert_legacy(config, task.pop("notifications"), label)
|
|
else:
|
|
notify = task.pop("notify", None)
|
|
|
|
if not notify:
|
|
yield task
|
|
continue
|
|
|
|
format_kwargs = dict(
|
|
task=task,
|
|
config=config.__dict__,
|
|
)
|
|
|
|
def substitute(ctx):
|
|
"""Recursively find all strings in a simple nested dict (no lists),
|
|
and format them in-place using `format_kwargs`."""
|
|
for key, val in ctx.items():
|
|
if isinstance(val, str):
|
|
ctx[key] = val.format(**format_kwargs)
|
|
elif isinstance(val, dict):
|
|
ctx[key] = substitute(val)
|
|
return ctx
|
|
|
|
task.setdefault("routes", [])
|
|
for recipient in notify["recipients"]:
|
|
type = recipient["type"]
|
|
recipient.setdefault("status-type", "on-completed")
|
|
substitute(recipient)
|
|
|
|
if type == "email":
|
|
resolve_keyed_by(
|
|
recipient,
|
|
"address",
|
|
label,
|
|
**{
|
|
"level": config.params["level"],
|
|
"project": config.params["project"],
|
|
},
|
|
)
|
|
|
|
task["routes"].append(
|
|
f"notify.{type}.{recipient[_route_keys[type]]}.{recipient['status-type']}"
|
|
)
|
|
|
|
if "content" in notify:
|
|
task.setdefault("extra", {}).update(
|
|
{"notify": _convert_content(substitute(notify["content"]))}
|
|
)
|
|
yield task
|