Merge "Add new configuration options for task emails"

This commit is contained in:
Zuul 2024-07-24 01:57:40 +00:00 committed by Gerrit Code Review
commit 95360b775b
3 changed files with 221 additions and 39 deletions

View File

@ -14,6 +14,9 @@
from confspirator import groups
from confspirator import fields
from confspirator import types
from adjutant.common import constants
config_group = groups.ConfigGroup("workflow")
@ -39,23 +42,42 @@ config_group.register_child_config(
def _build_default_email_group(
group_name,
email_subject,
subject,
email_from,
email_to,
email_reply,
email_template,
email_html_template,
template,
html_template,
email_current_user,
emails,
):
email_group = groups.ConfigGroup(group_name)
email_group.register_child_config(
fields.StrConfig(
"subject",
help_text="Default email subject for this stage",
default=email_subject,
default=subject,
)
)
email_group.register_child_config(
fields.StrConfig(
"from", help_text="Default from email for this stage", default=email_from
"from",
help_text="Default from email for this stage",
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
default=email_from,
)
)
email_group.register_child_config(
fields.StrConfig(
"to",
help_text=(
"Send the email to the given email address. "
"If not set, the email will be sent to the "
"recipient email address determined by the action "
"being run."
),
regex=constants.EMAIL_WITH_TEMPLATE_REGEX,
default=email_to,
)
)
email_group.register_child_config(
@ -69,14 +91,32 @@ def _build_default_email_group(
fields.StrConfig(
"template",
help_text="Default email template for this stage",
default=email_template,
default=template,
)
)
email_group.register_child_config(
fields.StrConfig(
"html_template",
help_text="Default email html template for this stage",
default=email_html_template,
default=html_template,
)
)
email_group.register_child_config(
fields.BoolConfig(
"email_current_user",
help_text="Email the user who initiated the task",
default=email_current_user,
)
)
email_group.register_child_config(
fields.ListConfig(
"emails",
item_type=types.List(item_type=types.Dict()),
help_text=(
"Send more than one email, setting parameter overrides "
"for each specific email as required"
),
default=emails,
)
)
return email_group
@ -90,31 +130,40 @@ _task_defaults_group.register_child_config(_email_defaults_group)
_email_defaults_group.register_child_config(
_build_default_email_group(
group_name="initial",
email_subject="Task Confirmation",
email_reply="no-reply@example.com",
subject="Task Confirmation",
email_from="bounce+%(task_uuid)s@example.com",
email_template="initial.txt",
email_html_template=None,
email_to=None,
email_reply="no-reply@example.com",
template="initial.txt",
html_template=None,
email_current_user=False,
emails=[],
)
)
_email_defaults_group.register_child_config(
_build_default_email_group(
group_name="token",
email_subject="Task Token",
email_reply="no-reply@example.com",
subject="Task Token",
email_from="bounce+%(task_uuid)s@example.com",
email_template="token.txt",
email_html_template=None,
email_to=None,
email_reply="no-reply@example.com",
template="token.txt",
html_template=None,
email_current_user=False,
emails=[],
)
)
_email_defaults_group.register_child_config(
_build_default_email_group(
group_name="completed",
email_subject="Task Completed",
email_reply="no-reply@example.com",
subject="Task Completed",
email_from="bounce+%(task_uuid)s@example.com",
email_template="completed.txt",
email_html_template=None,
email_to=None,
email_reply="no-reply@example.com",
template="completed.txt",
html_template=None,
email_current_user=False,
emails=[],
)
)

View File

@ -22,6 +22,7 @@ from django.template import loader
from django.utils import timezone
from adjutant.api.models import Token
from adjutant.common import user_store
from adjutant.notifications.utils import create_notification
from adjutant.config import CONF
from adjutant import exceptions
@ -58,27 +59,109 @@ def create_token(task, expiry_time=None):
def send_stage_email(task, email_conf, token=None):
"""Send one or more stage emails for a task using the given configuration.
This also accepts ``None`` for ``email_conf``, in which case
no emails are sent.
:param task: Task to send the stage email for
:type task: Task
:param email_conf: Stage email configuration (if configured)
:type email_conf: confspirator.groups.GroupNamespace | None
:param token: Token to add to the email template, defaults to None
:type token: str | None, optional
"""
if not email_conf:
return
text_template = loader.get_template(
email_conf["template"], using="include_etc_templates"
)
html_template = email_conf["html_template"]
# Send one or more emails according to per-email configurations
# if provided. If not, send a single email using the stage-global
# email configuration values.
emails = email_conf["emails"] or [{}]
# For each per-email configuration, send a stage email using
# that configuration.
# We want to use the per-email configuration values if provided,
# but fall back to the stage-global email configuration value
# for any that are not.
for conf in emails:
_send_stage_email(
task=task,
token=token,
subject=conf.get("subject", email_conf["subject"]),
template=conf.get("template", email_conf["template"]),
html_template=conf.get(
"html_template",
email_conf["html_template"],
),
email_from=conf.get("from", email_conf["from"]),
email_to=conf.get("to", email_conf["to"]),
email_reply=conf.get("reply", email_conf["reply"]),
email_current_user=conf.get(
"email_current_user",
email_conf["email_current_user"],
),
)
def _send_stage_email(
task,
token,
subject,
template,
html_template,
email_from,
email_to,
email_reply,
email_current_user,
):
text_template = loader.get_template(template, using="include_etc_templates")
if html_template:
html_template = loader.get_template(
html_template, using="include_etc_templates"
)
# find our set of emails and actions that require email
emails = set()
actions = {}
# find our set of emails and actions that require email
# Fetch all possible email addresses that can be configured.
# Even if these are not actually used as the target email,
# they are made available in the email templates to be referenced.
if CONF.identity.username_is_email and "username" in task.keystone_user:
email_current_user_address = task.keystone_user["username"]
elif "user_id" in task.keystone_user:
id_manager = user_store.IdentityManager()
user = id_manager.get_user(task.keystone_user["user_id"])
email_current_user_address = user.email if user else None
else:
email_current_user_address = None
email_action_addresses = {}
for action in task.actions:
act = action.get_action()
email = act.get_email()
if email:
emails.add(email)
actions[str(act)] = act
action_name = str(act)
email_action_addresses[action_name] = email
actions[action_name] = act
if email_to:
emails.add(email_to)
elif email_current_user:
if not email_current_user_address:
notes = {
"errors": (
"Error: Unable to send update, "
"task email is configured to send to current user "
f"but no username or user ID found in task: {task.uuid}"
),
}
create_notification(task, notes, error=True)
return
emails.add(email_current_user_address)
else:
emails |= set(email_action_addresses.values())
if not emails:
return
@ -93,7 +176,20 @@ def send_stage_email(task, email_conf, token=None):
create_notification(task, notes, error=True)
return
context = {"task": task, "actions": actions}
# from_email is the return-path and is distinct from the
# message headers
from_email = email_from % {"task_uuid": task.uuid} if email_from else email_reply
email_address = emails.pop()
context = {
"task": task,
"actions": actions,
"from_address": from_email,
"reply_address": email_reply,
"email_address": email_address,
"email_current_user_address": email_current_user_address,
"email_action_addresses": email_action_addresses,
}
if token:
tokenurl = CONF.workflow.horizon_url
if not tokenurl.endswith("/"):
@ -104,28 +200,20 @@ def send_stage_email(task, email_conf, token=None):
try:
message = text_template.render(context)
# from_email is the return-path and is distinct from the
# message headers
from_email = email_conf["from"]
if not from_email:
from_email = email_conf["reply"]
elif "%(task_uuid)s" in from_email:
from_email = from_email % {"task_uuid": task.uuid}
# these are the message headers which will be visible to
# the email client.
headers = {
"X-Adjutant-Task-UUID": task.uuid,
# From needs to be set to be disctinct from return-path
"From": email_conf["reply"],
"Reply-To": email_conf["reply"],
"From": email_reply,
"Reply-To": email_reply,
}
email = EmailMultiAlternatives(
email_conf["subject"],
subject,
message,
from_email,
[emails.pop()],
[email_address],
headers=headers,
)

View File

@ -0,0 +1,45 @@
---
features:
- |
Added the ``to`` field to task stage email configurations, for setting
an arbitrary address to send task stage emails to.
- |
Added the ``email_current_user`` field to task stage email configurations,
for sending task stage emails to the user who initiated the task.
Set ``email_current_user`` to ``true`` to enable this behaviour.
- |
Added the ``from_address`` variable to task stage email template
contexts, allowing the address the email is being sent from internally
to be templated in task stage email bodies.
Note that this is not necessarily the same address that is set in the
``From`` header of the email. For that address, use
``reply_address`` instead.
- |
Added the ``reply_address`` variable to task stage email template
contexts, allowing the reply-to address sent to the recipient to be
templated in task stage email bodies.
- |
Added the ``email_address`` variable to task stage email template contexts,
allowing the recipient email address to be templated in task stage email
bodies.
- |
Added the ``email_current_user_address`` variable to task stage email
template contexts, which exposes the email address of the user that
initiated the task for use in task stage email templates.
Note that depending on the task being run this value may not be
available for use, in which case it will be set to ``None``.
- |
Added the ``email_action_addresses`` variable to task stage email
template contexts, which exposes a dictionary mapping task actions
to their recipient email addresses for use in task stage email templates.
Note that depending on the task being run there may not be an email
address available for certain actions, in which case the dictionary will
not store a value for those tasks. If no tasks have any recipient email
addresses, the dictionary will be empty.
- |
Multiple emails can now be sent per task stage using the new ``emails``
configuration field. To send multiple emails per task stage, define a list
of emails to be sent as ``emails``, with per-email configuration set in
the list elements. If a value is not set per-email, the value set in the
stage configuration will be used, and if that is unset, the default value
will be used.