This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 923f5a5 Make a separate hook for interacting with the Pagerduty
Events API (#18784)
923f5a5 is described below
commit 923f5a5912785649be7e61c8ea32a0bd6dc426d8
Author: Guido Tournois <[email protected]>
AuthorDate: Tue Oct 12 20:00:59 2021 +0200
Make a separate hook for interacting with the Pagerduty Events API (#18784)
---
airflow/providers/pagerduty/hooks/pagerduty.py | 89 +++++++++--------
.../hooks/{pagerduty.py => pagerduty_events.py} | 80 +++++++--------
airflow/providers/pagerduty/provider.yaml | 3 +
tests/providers/pagerduty/hooks/test_pagerduty.py | 107 ++++++++++++---------
.../pagerduty/hooks/test_pagerduty_events.py | 54 +++++++++++
5 files changed, 198 insertions(+), 135 deletions(-)
diff --git a/airflow/providers/pagerduty/hooks/pagerduty.py
b/airflow/providers/pagerduty/hooks/pagerduty.py
index a908ddd..17e1edb 100644
--- a/airflow/providers/pagerduty/hooks/pagerduty.py
+++ b/airflow/providers/pagerduty/hooks/pagerduty.py
@@ -16,19 +16,28 @@
# specific language governing permissions and limitations
# under the License.
"""Hook for sending or receiving data from PagerDuty as well as creating
PagerDuty incidents."""
+import warnings
from typing import Any, Dict, List, Optional
import pdpyras
from airflow.exceptions import AirflowException
from airflow.hooks.base import BaseHook
+from airflow.providers.pagerduty.hooks.pagerduty_events import
PagerdutyEventsHook
class PagerdutyHook(BaseHook):
"""
- Takes both PagerDuty API token directly and connection that has PagerDuty
API token.
+ The PagerdutyHook can be used to interact with both the PagerDuty API and
the PagerDuty Events API.
+ Takes both PagerDuty API token directly and connection that has PagerDuty
API token.
If both supplied, PagerDuty API token will be used.
+ In these cases, the PagerDuty API token refers to an account token:
+
https://support.pagerduty.com/docs/generating-api-keys#generating-a-general-access-rest-api-key
+
https://support.pagerduty.com/docs/generating-api-keys#generating-a-personal-rest-api-key
+
+ In order to send events (with the Pagerduty Events API), you will also
need to specify the
+ routing_key (or Integration key) in the ``extra`` field
:param token: PagerDuty API token
:param pagerduty_conn_id: connection that has PagerDuty API token in the
password field
@@ -39,6 +48,16 @@ class PagerdutyHook(BaseHook):
conn_type = "pagerduty"
hook_name = "Pagerduty"
+ @staticmethod
+ def get_ui_field_behaviour() -> Dict:
+ """Returns custom field behaviour"""
+ return {
+ "hidden_fields": ['port', 'login', 'schema', 'host'],
+ "relabeling": {
+ 'password': 'Pagerduty API token',
+ },
+ }
+
def __init__(self, token: Optional[str] = None, pagerduty_conn_id:
Optional[str] = None) -> None:
super().__init__()
self.routing_key = None
@@ -75,8 +94,8 @@ class PagerdutyHook(BaseHook):
self,
summary: str,
severity: str,
- source: str = 'airflow',
- action: str = 'trigger',
+ source: str = "airflow",
+ action: str = "trigger",
routing_key: Optional[str] = None,
dedup_key: Optional[str] = None,
custom_details: Optional[Any] = None,
@@ -129,45 +148,25 @@ class PagerdutyHook(BaseHook):
:return: PagerDuty Events API v2 response.
:rtype: dict
"""
- if routing_key is None:
- routing_key = self.routing_key
- if routing_key is None:
- raise AirflowException('No routing/integration key specified.')
- payload = {
- "summary": summary,
- "severity": severity,
- "source": source,
- }
- if custom_details is not None:
- payload["custom_details"] = custom_details
- if component:
- payload["component"] = component
- if group:
- payload["group"] = group
- if class_type:
- payload["class"] = class_type
-
- actions = ('trigger', 'acknowledge', 'resolve')
- if action not in actions:
- raise ValueError(f"Event action must be one of: {',
'.join(actions)}")
- data = {
- "event_action": action,
- "payload": payload,
- }
- if dedup_key:
- data["dedup_key"] = dedup_key
- elif action != 'trigger':
- raise ValueError(
- "The dedup_key property is required for event_action=%s
events, and it must \
- be a string."
- % action
- )
- if images is not None:
- data["images"] = images
- if links is not None:
- data["links"] = links
-
- session = pdpyras.EventsAPISession(routing_key)
- resp = session.post('/v2/enqueue', json=data)
- resp.raise_for_status()
- return resp.json()
+ warnings.warn(
+ "This method will be deprecated. Please use the "
+ "`airflow.providers.pagerduty.hooks.PagerdutyEventsHook` to
interact with the Events API",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ routing_key = routing_key or self.routing_key
+
+ return PagerdutyEventsHook(integration_key=routing_key).create_event(
+ summary=summary,
+ severity=severity,
+ source=source,
+ action=action,
+ dedup_key=dedup_key,
+ custom_details=custom_details,
+ group=group,
+ component=component,
+ class_type=class_type,
+ images=images,
+ links=links,
+ )
diff --git a/airflow/providers/pagerduty/hooks/pagerduty.py
b/airflow/providers/pagerduty/hooks/pagerduty_events.py
similarity index 69%
copy from airflow/providers/pagerduty/hooks/pagerduty.py
copy to airflow/providers/pagerduty/hooks/pagerduty_events.py
index a908ddd..f133df2 100644
--- a/airflow/providers/pagerduty/hooks/pagerduty.py
+++ b/airflow/providers/pagerduty/hooks/pagerduty_events.py
@@ -24,52 +24,50 @@ from airflow.exceptions import AirflowException
from airflow.hooks.base import BaseHook
-class PagerdutyHook(BaseHook):
+class PagerdutyEventsHook(BaseHook):
"""
- Takes both PagerDuty API token directly and connection that has PagerDuty
API token.
+ This class can be used to interact with the Pagerduty Events API.
- If both supplied, PagerDuty API token will be used.
+ It takes both an Events API token and a PagerDuty connection with the
Events API token
+ (i.e. Integration key) as the password/Pagerduty API token. If both
supplied, the token will be used.
- :param token: PagerDuty API token
- :param pagerduty_conn_id: connection that has PagerDuty API token in the
password field
+ :param integration_key: PagerDuty Events API token
+ :param pagerduty_conn_id: connection that has PagerDuty integration key in
the Pagerduty API token field
"""
- conn_name_attr = "pagerduty_conn_id"
- default_conn_name = "pagerduty_default"
- conn_type = "pagerduty"
- hook_name = "Pagerduty"
+ conn_name_attr = "pagerduty_events_conn_id"
+ default_conn_name = "pagerduty_events_default"
+ conn_type = "pagerduty_events"
+ hook_name = "Pagerduty Events"
+
+ @staticmethod
+ def get_ui_field_behaviour() -> Dict:
+ """Returns custom field behaviour"""
+ return {
+ "hidden_fields": ['port', 'login', 'schema', 'host', 'extra'],
+ "relabeling": {
+ 'password': 'Pagerduty Integration key',
+ },
+ }
- def __init__(self, token: Optional[str] = None, pagerduty_conn_id:
Optional[str] = None) -> None:
+ def __init__(
+ self, integration_key: Optional[str] = None, pagerduty_events_conn_id:
Optional[str] = None
+ ) -> None:
super().__init__()
- self.routing_key = None
+ self.integration_key = None
self._session = None
- if pagerduty_conn_id is not None:
- conn = self.get_connection(pagerduty_conn_id)
- self.token = conn.get_password()
-
- routing_key = conn.extra_dejson.get("routing_key")
- if routing_key:
- self.routing_key = routing_key
-
- if token is not None: # token takes higher priority
- self.token = token
-
- if self.token is None:
- raise AirflowException('Cannot get token: No valid api token nor
pagerduty_conn_id supplied.')
+ if pagerduty_events_conn_id is not None:
+ conn = self.get_connection(pagerduty_events_conn_id)
+ self.integration_key = conn.get_password()
- def get_session(self) -> pdpyras.APISession:
- """
- Returns `pdpyras.APISession` for use with sending or receiving data
through the PagerDuty REST API.
-
- The `pdpyras` library supplies a class `pdpyras.APISession` extending
`requests.Session` from the
- Requests HTTP library.
+ if integration_key is not None: # token takes higher priority
+ self.integration_key = integration_key
- Documentation on how to use the `APISession` class can be found at:
- https://pagerduty.github.io/pdpyras/#data-access-abstraction
- """
- self._session = pdpyras.APISession(self.token)
- return self._session
+ if self.integration_key is None:
+ raise AirflowException(
+ 'Cannot get token: No valid integration key nor
pagerduty_events_conn_id supplied.'
+ )
def create_event(
self,
@@ -77,7 +75,6 @@ class PagerdutyHook(BaseHook):
severity: str,
source: str = 'airflow',
action: str = 'trigger',
- routing_key: Optional[str] = None,
dedup_key: Optional[str] = None,
custom_details: Optional[Any] = None,
group: Optional[str] = None,
@@ -99,9 +96,6 @@ class PagerdutyHook(BaseHook):
:param action: Event action, needs to be one of: trigger, acknowledge,
resolve. Default to trigger if not specified.
:type action: str
- :param routing_key: Integration key. If not specified, will try to read
- from connection's extra json blob.
- :type routing_key: str
:param dedup_key: A string which identifies the alert triggered for
the given event.
Required for the actions acknowledge and resolve.
:type dedup_key: str
@@ -129,10 +123,6 @@ class PagerdutyHook(BaseHook):
:return: PagerDuty Events API v2 response.
:rtype: dict
"""
- if routing_key is None:
- routing_key = self.routing_key
- if routing_key is None:
- raise AirflowException('No routing/integration key specified.')
payload = {
"summary": summary,
"severity": severity,
@@ -167,7 +157,7 @@ class PagerdutyHook(BaseHook):
if links is not None:
data["links"] = links
- session = pdpyras.EventsAPISession(routing_key)
- resp = session.post('/v2/enqueue', json=data)
+ session = pdpyras.EventsAPISession(self.integration_key)
+ resp = session.post("/v2/enqueue", json=data)
resp.raise_for_status()
return resp.json()
diff --git a/airflow/providers/pagerduty/provider.yaml
b/airflow/providers/pagerduty/provider.yaml
index af1aaba..dae2a1c 100644
--- a/airflow/providers/pagerduty/provider.yaml
+++ b/airflow/providers/pagerduty/provider.yaml
@@ -39,8 +39,11 @@ integrations:
connection-types:
- hook-class-name: airflow.providers.pagerduty.hooks.pagerduty.PagerdutyHook
connection-type: pagerduty
+ - hook-class-name:
airflow.providers.pagerduty.hooks.pagerduty_events.PagerdutyEventsHook
+ connection-type: pagerduty_events
hooks:
- integration-name: Pagerduty
python-modules:
- airflow.providers.pagerduty.hooks.pagerduty
+ - airflow.providers.pagerduty.hooks.pagerduty_events
diff --git a/tests/providers/pagerduty/hooks/test_pagerduty.py
b/tests/providers/pagerduty/hooks/test_pagerduty.py
index 1c1348d..a2b1b1c 100644
--- a/tests/providers/pagerduty/hooks/test_pagerduty.py
+++ b/tests/providers/pagerduty/hooks/test_pagerduty.py
@@ -15,55 +15,51 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-import unittest
+from unittest import mock
-import requests_mock
+import pytest
from airflow.models import Connection
from airflow.providers.pagerduty.hooks.pagerduty import PagerdutyHook
-from airflow.utils.session import provide_session
+from airflow.providers.pagerduty.hooks.pagerduty_events import
PagerdutyEventsHook
+from airflow.utils import db
DEFAULT_CONN_ID = "pagerduty_default"
-class TestPagerdutyHook(unittest.TestCase):
- @classmethod
- @provide_session
- def setUpClass(cls, session=None):
- session.add(
- Connection(
- conn_id=DEFAULT_CONN_ID,
- conn_type='http',
- password="pagerduty_token",
- extra='{"routing_key": "route"}',
- )
[email protected](scope="class")
+def pagerduty_connections():
+ db.merge_conn(
+ Connection(
+ conn_id=DEFAULT_CONN_ID,
+ conn_type="pagerduty",
+ password="token",
+ extra='{"routing_key": "integration_key"}',
)
- session.commit()
+ )
+ db.merge_conn(
+ Connection(
+ conn_id="pagerduty_no_extra", conn_type="pagerduty",
password="pagerduty_token_without_extra"
+ ),
+ )
- @provide_session
- def test_without_routing_key_extra(self, session):
- session.add(
- Connection(
- conn_id="pagerduty_no_extra",
- conn_type='http',
- password="pagerduty_token_without_extra",
- )
- )
- session.commit()
- hook = PagerdutyHook(pagerduty_conn_id="pagerduty_no_extra")
- assert hook.token == 'pagerduty_token_without_extra', 'token
initialised.'
- assert hook.routing_key is None, 'default routing key skipped.'
- def test_get_token_from_password(self):
+class TestPagerdutyHook:
+ def test_get_token_from_password(self, pagerduty_connections):
hook = PagerdutyHook(pagerduty_conn_id=DEFAULT_CONN_ID)
- assert hook.token == 'pagerduty_token', 'token initialised.'
+ assert hook.token == "token", "token initialised."
+ assert hook.routing_key == "integration_key"
+
+ def test_without_routing_key_extra(self):
+ hook = PagerdutyHook(pagerduty_conn_id="pagerduty_no_extra")
+ assert hook.token == "pagerduty_token_without_extra", "token
initialised."
+ assert hook.routing_key is None, "default routing key skipped."
def test_token_parameter_override(self):
hook = PagerdutyHook(token="pagerduty_param_token",
pagerduty_conn_id=DEFAULT_CONN_ID)
- assert hook.token == 'pagerduty_param_token', 'token initialised.'
+ assert hook.token == "pagerduty_param_token", "token initialised."
- @requests_mock.mock()
- def test_get_service(self, m):
+ def test_get_service(self, requests_mock):
hook = PagerdutyHook(pagerduty_conn_id=DEFAULT_CONN_ID)
mock_response_body = {
"id": "PZYX321",
@@ -73,24 +69,45 @@ class TestPagerdutyHook(unittest.TestCase):
"summary": "Apache Airflow",
"self": "https://api.pagerduty.com/services/PZYX321",
}
- m.get('https://api.pagerduty.com/services/PZYX321', json={"service":
mock_response_body})
+ requests_mock.get("https://api.pagerduty.com/services/PZYX321",
json={"service": mock_response_body})
session = hook.get_session()
- resp = session.rget('/services/PZYX321')
+ resp = session.rget("/services/PZYX321")
assert resp == mock_response_body
- @requests_mock.mock()
- def test_create_event(self, m):
+ @mock.patch.object(PagerdutyEventsHook, "__init__")
+ @mock.patch.object(PagerdutyEventsHook, "create_event")
+ def test_create_event(self, events_hook_create_event, events_hook_init):
+ events_hook_init.return_value = None
hook = PagerdutyHook(pagerduty_conn_id=DEFAULT_CONN_ID)
- mock_response_body = {
- "status": "success",
- "message": "Event processed",
- "dedup_key": "samplekeyhere",
- }
- m.post('https://events.pagerduty.com/v2/enqueue',
json=mock_response_body)
- resp = hook.create_event(
+ hook.create_event(
+ summary="test",
+ source="airflow_test",
+ severity="error",
+ )
+ events_hook_init.assert_called_with(integration_key="integration_key")
+ events_hook_create_event.assert_called_with(
+ summary="test",
+ source="airflow_test",
+ severity="error",
+ action="trigger",
+ dedup_key=None,
+ custom_details=None,
+ group=None,
+ component=None,
+ class_type=None,
+ images=None,
+ links=None,
+ )
+
+ @mock.patch.object(PagerdutyEventsHook, "create_event",
mock.MagicMock(return_value=None))
+ @mock.patch.object(PagerdutyEventsHook, "__init__")
+ def test_create_event_override(self, events_hook_init):
+ events_hook_init.return_value = None
+ hook = PagerdutyHook(pagerduty_conn_id=DEFAULT_CONN_ID)
+ hook.create_event(
routing_key="different_key",
summary="test",
source="airflow_test",
severity="error",
)
- assert resp == mock_response_body
+ events_hook_init.assert_called_with(integration_key="different_key")
diff --git a/tests/providers/pagerduty/hooks/test_pagerduty_events.py
b/tests/providers/pagerduty/hooks/test_pagerduty_events.py
new file mode 100644
index 0000000..dc1f4f1
--- /dev/null
+++ b/tests/providers/pagerduty/hooks/test_pagerduty_events.py
@@ -0,0 +1,54 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import pytest
+
+from airflow.models import Connection
+from airflow.providers.pagerduty.hooks.pagerduty import PagerdutyEventsHook
+from airflow.utils import db
+
+DEFAULT_CONN_ID = "pagerduty_events_default"
+
+
[email protected](scope="class")
+def events_connections():
+ db.merge_conn(Connection(conn_id=DEFAULT_CONN_ID,
conn_type="pagerduty_events", password="events_token"))
+
+
+class TestPagerdutyEventsHook:
+ def test_get_integration_key_from_password(self, events_connections):
+ hook = PagerdutyEventsHook(pagerduty_events_conn_id=DEFAULT_CONN_ID)
+ assert hook.integration_key == "events_token", "token initialised."
+
+ def test_token_parameter_override(self, events_connections):
+ hook = PagerdutyEventsHook(integration_key="override_key",
pagerduty_events_conn_id=DEFAULT_CONN_ID)
+ assert hook.integration_key == "override_key", "token initialised."
+
+ def test_create_event(self, requests_mock, events_connections):
+ hook = PagerdutyEventsHook(pagerduty_events_conn_id=DEFAULT_CONN_ID)
+ mock_response_body = {
+ "status": "success",
+ "message": "Event processed",
+ "dedup_key": "samplekeyhere",
+ }
+ requests_mock.post("https://events.pagerduty.com/v2/enqueue",
json=mock_response_body)
+ resp = hook.create_event(
+ summary="test",
+ source="airflow_test",
+ severity="error",
+ )
+ assert resp == mock_response_body