From 5dcc514d72bcaef75b4d4fcf39a8fb1979a8c055 Mon Sep 17 00:00:00 2001 From: Pavel Fateev Date: Tue, 9 Jul 2024 14:58:09 -0700 Subject: [PATCH 1/7] add readme --- confluence_to_slack/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 confluence_to_slack/README.md diff --git a/confluence_to_slack/README.md b/confluence_to_slack/README.md new file mode 100644 index 00000000..59c4d20b --- /dev/null +++ b/confluence_to_slack/README.md @@ -0,0 +1,17 @@ +# Confluence To Slack Workflow + +This real-life workflow demonstrates the integration between two popular services. + +## Benefits +- **Small overhead**: Run the `ak` server, deploy the project, and write code. +- **Filtering**: Add filters in the configuration to limit the number of times your code is triggered, or filter data in the code itself. This workflow demonstrates both. + +## How It Works +- **Trigger**: A new Confluence page is created in a designated space. +- **Result**: A Slack message is sent to a selected channel containing data from the newly created Confluence page. + +## Known Limitations +- Confluence returns HTML, and this program does not format it in any way. The purpose of this workflow is to demonstrate how data can move between different services. Desired formatting can be easily added to suit individual needs. + +## Additional Comment +- Environment variables are set in `autokitteh.yaml` (e.g., Slack channel, Confluence page, etc.). From b55a9fbbc7550482e3cc422c4d39478388c530fd Mon Sep 17 00:00:00 2001 From: Pasha Fateev Date: Thu, 11 Jul 2024 09:48:01 -0700 Subject: [PATCH 2/7] Update confluence_to_slack/README.md Co-authored-by: Daniel Abraham --- confluence_to_slack/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/confluence_to_slack/README.md b/confluence_to_slack/README.md index 59c4d20b..3d9cd3c3 100644 --- a/confluence_to_slack/README.md +++ b/confluence_to_slack/README.md @@ -3,6 +3,7 @@ This real-life workflow demonstrates the integration between two popular services. ## Benefits + - **Small overhead**: Run the `ak` server, deploy the project, and write code. - **Filtering**: Add filters in the configuration to limit the number of times your code is triggered, or filter data in the code itself. This workflow demonstrates both. From 2a30209ff5392e116e74d62c65dd5f5cab757e21 Mon Sep 17 00:00:00 2001 From: Pasha Fateev Date: Thu, 11 Jul 2024 09:48:15 -0700 Subject: [PATCH 3/7] Update confluence_to_slack/README.md Co-authored-by: Daniel Abraham --- confluence_to_slack/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/confluence_to_slack/README.md b/confluence_to_slack/README.md index 3d9cd3c3..69c179f7 100644 --- a/confluence_to_slack/README.md +++ b/confluence_to_slack/README.md @@ -8,6 +8,7 @@ This real-life workflow demonstrates the integration between two popular service - **Filtering**: Add filters in the configuration to limit the number of times your code is triggered, or filter data in the code itself. This workflow demonstrates both. ## How It Works + - **Trigger**: A new Confluence page is created in a designated space. - **Result**: A Slack message is sent to a selected channel containing data from the newly created Confluence page. From 139d03c4962cef84598ae53bdd010d38933b2e52 Mon Sep 17 00:00:00 2001 From: Pasha Fateev Date: Thu, 11 Jul 2024 09:48:59 -0700 Subject: [PATCH 4/7] Update confluence_to_slack/README.md Co-authored-by: Daniel Abraham --- confluence_to_slack/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/confluence_to_slack/README.md b/confluence_to_slack/README.md index 69c179f7..b0ab6419 100644 --- a/confluence_to_slack/README.md +++ b/confluence_to_slack/README.md @@ -13,6 +13,7 @@ This real-life workflow demonstrates the integration between two popular service - **Result**: A Slack message is sent to a selected channel containing data from the newly created Confluence page. ## Known Limitations + - Confluence returns HTML, and this program does not format it in any way. The purpose of this workflow is to demonstrate how data can move between different services. Desired formatting can be easily added to suit individual needs. ## Additional Comment From bf93a25ab065a573345d384b5d41937ae468e029 Mon Sep 17 00:00:00 2001 From: Pasha Fateev Date: Thu, 11 Jul 2024 09:49:11 -0700 Subject: [PATCH 5/7] Update confluence_to_slack/README.md Co-authored-by: Daniel Abraham --- confluence_to_slack/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confluence_to_slack/README.md b/confluence_to_slack/README.md index b0ab6419..250ca499 100644 --- a/confluence_to_slack/README.md +++ b/confluence_to_slack/README.md @@ -17,4 +17,5 @@ This real-life workflow demonstrates the integration between two popular service - Confluence returns HTML, and this program does not format it in any way. The purpose of this workflow is to demonstrate how data can move between different services. Desired formatting can be easily added to suit individual needs. ## Additional Comment -- Environment variables are set in `autokitteh.yaml` (e.g., Slack channel, Confluence page, etc.). + +- Environment variables are set in [`autokitteh.yaml`](./autokitteh.yaml) (e.g., Slack channel, Confluence page, etc.). From bb2943d48976dcc557b830094e0a3eba02c32918 Mon Sep 17 00:00:00 2001 From: Daniel Abraham Date: Thu, 11 Jul 2024 06:35:35 +0300 Subject: [PATCH 6/7] ENG-1047: AWS Health notifications in Slack (#16) Not tested with real data - see https://docs.aws.amazon.com/health/latest/ug/health-api.html > You must have a Business, Enterprise On-Ramp, or Enterprise Support plan from [AWS Support](https://aws.amazon.com/premiumsupport/) to use the AWS Health API. If you call the AWS Health API from an AWS account that doesn't have a Business, Enterprise On-Ramp, or Enterprise Support plan, you receive a SubscriptionRequiredException error. --- aws_health_to_slack/autokitteh.yaml | 30 +++++++ aws_health_to_slack/program.py | 116 ++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 aws_health_to_slack/autokitteh.yaml create mode 100644 aws_health_to_slack/program.py diff --git a/aws_health_to_slack/autokitteh.yaml b/aws_health_to_slack/autokitteh.yaml new file mode 100644 index 00000000..d0f85da5 --- /dev/null +++ b/aws_health_to_slack/autokitteh.yaml @@ -0,0 +1,30 @@ +# This YAML file is a declarative manifest that describes the setup +# of an AutoKitteh project that announces AWS health events in Slack +# channels, based on resource ownership data in a Google Sheet. +# +# Before deploying this AutoKitteh project, set "GOOGLE_SHEET_URL" +# to your own resource ownership data, like in the template. +# +# After creating this AutoKitteh project by applying this file, +# initialize its AWS, Google Sheets, and Slack connections. + +version: v1 + +project: + name: aws_health_to_slack + vars: + - name: GOOGLE_SHEET_URL + value: https://docs.google.com/spreadsheets/d/1PalmLwSZOPW9k668_jU-wFI5xCj88a4mDfNUtJAupMQ/ + - name: TRIGGER_INTERVAL + value: 1m + connections: + - name: aws_connection + integration: aws + - name: google_sheets_connection + integration: googlesheets + - name: slack_connection + integration: slack + triggers: + - name: every_minute + schedule: "@every 1m" + call: program.py:on_schedule diff --git a/aws_health_to_slack/program.py b/aws_health_to_slack/program.py new file mode 100644 index 00000000..9557e29b --- /dev/null +++ b/aws_health_to_slack/program.py @@ -0,0 +1,116 @@ +"""Announce AWS health events in Slack, based on resource ownership in Google Sheet. + +Documentation: +https://docs.aws.amazon.com/health/ +https://aws.amazon.com/blogs/mt/tag/aws-health-api/ +""" + +from datetime import UTC, datetime, timedelta +import json +import os +import re + +import autokitteh +from autokitteh.aws import boto3_client +from autokitteh.google import google_id, google_sheets_client +from autokitteh.slack import slack_client + + +url = os.getenv("GOOGLE_SHEET_URL") + + +def on_schedule(_): + """Workflow's entry-point.""" + slack_channels = _read_google_sheet() + events = _aws_health_events() + events_by_arn = {event["arn"]: event for event in events} + + for entity in _affected_aws_entities(events): + project = entity.get("tags", {}).get("project") + if not project: + print(f"Error: AWS entity without project tag: {entity}") + continue + + channel = slack_channels.get(project) + affecting_events = [events_by_arn[arn] for arn in entity["eventArns"]] + _post_slack_message(project, channel, entity, affecting_events) + + +@autokitteh.activity +def _read_google_sheet() -> dict[str, str]: + """Read mapping of project tags to Slack channels from Google Sheet.""" + sheets = google_sheets_client("google_sheets_connection").spreadsheets().values() + rows = sheets.get(spreadsheetId=google_id(url), range="A:B").execute().get("values", []) + return {row[0].strip(): row[1].strip() for row in rows} + + +@autokitteh.activity +def _aws_health_events() -> list[dict]: + """List all recent AWS Health events. + + This function currently fetches events for a single AWS account: + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/health/client/describe_events.html + + With a bit more code, you can also fetch events for multiple ones: + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/health/client/describe_events_for_organization.html + """ + try: + mins = int(re.match(r"(\d+)m", os.getenv("TRIGGER_INTERVAL")).group(1)) + prev_check = datetime.now(UTC) - timedelta(minutes=mins) + filter = {"lastUpdatedTime": [{"from": prev_check}]} + + aws = boto3_client("aws_connection", "health") + resp = aws.describe_events(filter=filter) + events = resp.get("events", []) + + nextToken = resp.get("nextToken") + while nextToken: + resp = aws.describe_events(filter=filter, nextToken=nextToken) + events += resp.get("events", []) + nextToken = resp.get("nextToken") + + return events + + # TODO: More specific exception handling. + except Exception as e: + print(f"Error: {e}") + return [] + + +@autokitteh.activity +def _affected_aws_entities(events: list[dict]) -> list[dict]: + """List all AWS entities affected by the given AWS Health events. + + API Reference: + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/health/client/describe_affected_entities.html + """ + try: + aws = boto3_client("aws_connection", "health") + arns = [event["arn"] for event in events] + # Possible alternative: describe_affected_entities_for_organization. + resp = aws.describe_affected_entities(eventArn=arns) + entities = resp.get("entities", []) + + nextToken = resp.get("nextToken") + while nextToken: + resp = aws.describe_affected_entities(eventArn=arns, nextToken=nextToken) + entities += resp.get("entities", []) + nextToken = resp.get("nextToken") + + return entities + except Exception as e: + print(f"Error: {e}") + return [] + + +def _post_slack_message(channel, project, entity: dict, affecting_events: list[dict]): + if not channel: + print(f"Error: project {project!r} not found in {url}") + + text = f"This AWS resource:\n```\n{json.dumps(entity, indent=4)}\n```" + text += "\nis affected by these AWS Health events:" + for i, event in enumerate(affecting_events, 1): + text += f"\n{i}.\n```\n{json.dumps(event, indent=4)}\n```" + + print(f"Posting in Slack channel: {channel!r}") + slack_client("slack_connection").chat_postMessage(channel=channel, text=text) From e629cd1de9b9d1d4499e29bf1b5bb45288ffc609 Mon Sep 17 00:00:00 2001 From: Daniel Abraham Date: Thu, 11 Jul 2024 21:31:02 +0300 Subject: [PATCH 7/7] Fix formatting (#23) --- aws_health_to_slack/program.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_health_to_slack/program.py b/aws_health_to_slack/program.py index 9557e29b..1c85944a 100644 --- a/aws_health_to_slack/program.py +++ b/aws_health_to_slack/program.py @@ -40,8 +40,8 @@ def on_schedule(_): def _read_google_sheet() -> dict[str, str]: """Read mapping of project tags to Slack channels from Google Sheet.""" sheets = google_sheets_client("google_sheets_connection").spreadsheets().values() - rows = sheets.get(spreadsheetId=google_id(url), range="A:B").execute().get("values", []) - return {row[0].strip(): row[1].strip() for row in rows} + rows = sheets.get(spreadsheetId=google_id(url), range="A:B").execute() + return {row[0].strip(): row[1].strip() for row in rows.get("values", [])} @autokitteh.activity