Skip to content

Commit 405bc26

Browse files
Vitor-Avilamichael-s-molina
authored andcommitted
fix(Slack): Fix Slack recipients migration to V2 (#32336)
(cherry picked from commit d2e0e2b)
1 parent d0a5bd8 commit 405bc26

File tree

11 files changed

+317
-77
lines changed

11 files changed

+317
-77
lines changed

superset/commands/report/execute.py

Lines changed: 48 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
)
7373
from superset.tasks.utils import get_executor
7474
from superset.utils import json
75-
from superset.utils.core import HeaderDataType, override_user
75+
from superset.utils.core import HeaderDataType, override_user, recipients_string_to_list
7676
from superset.utils.csv import get_chart_csv_data, get_chart_dataframe
7777
from superset.utils.decorators import logs_context, transaction
7878
from superset.utils.pdf import build_pdf_from_screenshots
@@ -137,24 +137,40 @@ def update_report_schedule_slack_v2(self) -> None:
137137
if recipient.type == ReportRecipientType.SLACK:
138138
recipient.type = ReportRecipientType.SLACKV2
139139
slack_recipients = json.loads(recipient.recipient_config_json)
140+
# V1 method allowed to use leading `#` in the channel name
141+
channel_names = (slack_recipients["target"] or "").replace("#", "")
140142
# we need to ensure that existing reports can also fetch
141143
# ids from private channels
144+
channels = get_channels_with_search(
145+
search_string=channel_names,
146+
types=[
147+
SlackChannelTypes.PRIVATE,
148+
SlackChannelTypes.PUBLIC,
149+
],
150+
exact_match=True,
151+
)
152+
channels_list = recipients_string_to_list(channel_names)
153+
if len(channels_list) != len(channels):
154+
missing_channels = set(channels_list) - {
155+
channel["name"] for channel in channels
156+
}
157+
msg = (
158+
"Could not find the following channels: "
159+
f"{', '.join(missing_channels)}"
160+
)
161+
raise UpdateFailedError(msg)
162+
channel_ids = ",".join(channel["id"] for channel in channels)
142163
recipient.recipient_config_json = json.dumps(
143164
{
144-
"target": get_channels_with_search(
145-
slack_recipients["target"],
146-
types=[
147-
SlackChannelTypes.PRIVATE,
148-
SlackChannelTypes.PUBLIC,
149-
],
150-
)
165+
"target": channel_ids,
151166
}
152167
)
153168
except Exception as ex:
154-
logger.warning(
155-
"Failed to update slack recipients to v2: %s", str(ex), exc_info=True
156-
)
157-
raise UpdateFailedError from ex
169+
# Revert to v1 to preserve configuration (requires manual fix)
170+
recipient.type = ReportRecipientType.SLACK
171+
msg = f"Failed to update slack recipients to v2: {str(ex)}"
172+
logger.exception(msg)
173+
raise UpdateFailedError(msg) from ex
158174

159175
def create_log(self, error_message: Optional[str] = None) -> None:
160176
"""
@@ -553,30 +569,32 @@ def _send(
553569
for recipient in recipients:
554570
notification = create_notification(recipient, notification_content)
555571
try:
556-
if app.config["ALERT_REPORTS_NOTIFICATION_DRY_RUN"]:
572+
try:
573+
if app.config["ALERT_REPORTS_NOTIFICATION_DRY_RUN"]:
574+
logger.info(
575+
"Would send notification for alert %s, to %s. "
576+
"ALERT_REPORTS_NOTIFICATION_DRY_RUN is enabled, "
577+
"set it to False to send notifications.",
578+
self._report_schedule.name,
579+
recipient.recipient_config_json,
580+
)
581+
else:
582+
notification.send()
583+
except SlackV1NotificationError as ex:
584+
# The slack notification should be sent with the v2 api
557585
logger.info(
558-
"Would send notification for alert %s, to %s. "
559-
"ALERT_REPORTS_NOTIFICATION_DRY_RUN is enabled, "
560-
"set it to False to send notifications.",
561-
self._report_schedule.name,
562-
recipient.recipient_config_json,
586+
"Attempting to upgrade the report to Slackv2: %s", str(ex)
563587
)
564-
else:
565-
notification.send()
566-
except SlackV1NotificationError as ex:
567-
# The slack notification should be sent with the v2 api
568-
logger.info("Attempting to upgrade the report to Slackv2: %s", str(ex))
569-
try:
570588
self.update_report_schedule_slack_v2()
571589
recipient.type = ReportRecipientType.SLACKV2
572590
notification = create_notification(recipient, notification_content)
573591
notification.send()
574-
except (UpdateFailedError, NotificationParamException) as err:
575-
# log the error but keep processing the report with SlackV1
576-
logger.warning(
577-
"Failed to update slack recipients to v2: %s", str(err)
578-
)
579-
except (NotificationError, SupersetException) as ex:
592+
except (
593+
UpdateFailedError,
594+
NotificationParamException,
595+
NotificationError,
596+
SupersetException,
597+
) as ex:
580598
# collect errors but keep processing them
581599
notification_errors.append(
582600
SupersetError(

superset/reports/notifications/slack.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
)
4444
from superset.reports.notifications.slack_mixin import SlackMixin
4545
from superset.utils import json
46-
from superset.utils.core import get_email_address_list
46+
from superset.utils.core import recipients_string_to_list
4747
from superset.utils.decorators import statsd_gauge
4848
from superset.utils.slack import (
4949
get_slack_client,
@@ -70,7 +70,7 @@ def _get_channel(self) -> str:
7070
"""
7171
recipient_str = json.loads(self._recipient.recipient_config_json)["target"]
7272

73-
return ",".join(get_email_address_list(recipient_str))
73+
return ",".join(recipients_string_to_list(recipient_str))
7474

7575
def _get_inline_files(
7676
self,

superset/reports/notifications/slackv2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
)
4343
from superset.reports.notifications.slack_mixin import SlackMixin
4444
from superset.utils import json
45-
from superset.utils.core import get_email_address_list
45+
from superset.utils.core import recipients_string_to_list
4646
from superset.utils.decorators import statsd_gauge
4747
from superset.utils.slack import get_slack_client
4848

@@ -64,7 +64,7 @@ def _get_channels(self) -> List[str]:
6464
""" # noqa: E501
6565
recipient_str = json.loads(self._recipient.recipient_config_json)["target"]
6666

67-
return get_email_address_list(recipient_str)
67+
return recipients_string_to_list(recipient_str)
6868

6969
def _get_inline_files(
7070
self,

superset/reports/schemas.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from croniter import croniter
2020
from flask import current_app
2121
from flask_babel import gettext as _
22-
from marshmallow import fields, Schema, validate, validates, validates_schema
22+
from marshmallow import EXCLUDE, fields, Schema, validate, validates, validates_schema
2323
from marshmallow.validate import Length, Range, ValidationError
2424
from pytz import all_timezones
2525

@@ -399,3 +399,17 @@ def validate_custom_width(
399399
max=max_width,
400400
)
401401
)
402+
403+
404+
class SlackChannelSchema(Schema):
405+
"""
406+
Schema to load Slack channels, set to ignore any fields not used by Superset.
407+
"""
408+
409+
class Meta:
410+
unknown = EXCLUDE
411+
412+
id = fields.String()
413+
name = fields.String()
414+
is_member = fields.Boolean()
415+
is_private = fields.Boolean()

superset/utils/core.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ def send_email_smtp( # pylint: disable=invalid-name,too-many-arguments,too-many
700700
'[email protected]', 'foo', '<b>Foo</b> bar',['/dev/null'], dryrun=True)
701701
"""
702702
smtp_mail_from = config["SMTP_MAIL_FROM"]
703-
smtp_mail_to = get_email_address_list(to)
703+
smtp_mail_to = recipients_string_to_list(to)
704704

705705
msg = MIMEMultipart(mime_subtype)
706706
msg["Subject"] = subject
@@ -711,14 +711,14 @@ def send_email_smtp( # pylint: disable=invalid-name,too-many-arguments,too-many
711711

712712
recipients = smtp_mail_to
713713
if cc:
714-
smtp_mail_cc = get_email_address_list(cc)
714+
smtp_mail_cc = recipients_string_to_list(cc)
715715
msg["Cc"] = ", ".join(smtp_mail_cc)
716716
recipients = recipients + smtp_mail_cc
717717

718718
smtp_mail_bcc = []
719719
if bcc:
720720
# don't add bcc in header
721-
smtp_mail_bcc = get_email_address_list(bcc)
721+
smtp_mail_bcc = recipients_string_to_list(bcc)
722722
recipients = recipients + smtp_mail_bcc
723723

724724
msg["Date"] = formatdate(localtime=True)
@@ -811,7 +811,13 @@ def send_mime_email(
811811
smtp.quit()
812812

813813

814-
def get_email_address_list(address_string: str) -> list[str]:
814+
def recipients_string_to_list(address_string: str | None) -> list[str]:
815+
"""
816+
Returns the list of target recipients for alerts and reports.
817+
818+
Strips values and converts a comma/semicolon separated
819+
string into a list.
820+
"""
815821
address_string_list: list[str] = []
816822
if isinstance(address_string, str):
817823
address_string_list = re.split(r",|\s|;", address_string)

superset/utils/slack.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525

2626
from superset import feature_flag_manager
2727
from superset.exceptions import SupersetException
28+
from superset.reports.schemas import SlackChannelSchema
2829
from superset.utils.backports import StrEnum
30+
from superset.utils.core import recipients_string_to_list
2931

3032
logger = logging.getLogger(__name__)
3133

@@ -51,7 +53,7 @@ def get_channels_with_search(
5153
limit: int = 999,
5254
types: Optional[list[SlackChannelTypes]] = None,
5355
exact_match: bool = False,
54-
) -> list[str]:
56+
) -> list[SlackChannelSchema]:
5557
"""
5658
The slack api is paginated but does not include search, so we need to fetch
5759
all channels and filter them ourselves
@@ -60,7 +62,8 @@ def get_channels_with_search(
6062

6163
try:
6264
client = get_slack_client()
63-
channels = []
65+
channel_schema = SlackChannelSchema()
66+
channels: list[SlackChannelSchema] = []
6467
cursor = None
6568
extra_params = {}
6669
extra_params["types"] = ",".join(types) if types else None
@@ -69,29 +72,27 @@ def get_channels_with_search(
6972
response = client.conversations_list(
7073
limit=limit, cursor=cursor, exclude_archived=True, **extra_params
7174
)
72-
channels.extend(response.data["channels"])
75+
channels.extend(
76+
channel_schema.load(channel) for channel in response.data["channels"]
77+
)
7378
cursor = response.data.get("response_metadata", {}).get("next_cursor")
7479
if not cursor:
7580
break
7681

7782
# The search string can be multiple channels separated by commas
7883
if search_string:
79-
search_array = [
80-
search.lower()
81-
for search in (search_string.split(",") if search_string else [])
82-
]
83-
84+
search_array = recipients_string_to_list(search_string)
8485
channels = [
8586
channel
8687
for channel in channels
8788
if any(
8889
(
89-
search == channel["name"].lower()
90-
or search == channel["id"].lower()
90+
search.lower() == channel["name"].lower()
91+
or search.lower() == channel["id"].lower()
9192
if exact_match
9293
else (
93-
search in channel["name"].lower()
94-
or search in channel["id"].lower()
94+
search.lower() in channel["name"].lower()
95+
or search.lower() in channel["id"].lower()
9596
)
9697
)
9798
for search in search_array

0 commit comments

Comments
 (0)