Skip to content
This repository was archived by the owner on Aug 4, 2023. It is now read-only.

Commit cd1f391

Browse files
committed
Add support to multiple notification channels, including NewRelic
1 parent 52d591b commit cd1f391

File tree

5 files changed

+135
-26
lines changed

5 files changed

+135
-26
lines changed

flamingo/models/deployment.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from gcp_pilot.datastore import Document, EmbeddedDocument
66

7-
from services.notifiers import ChatNotifier
87

98
if TYPE_CHECKING:
109
from models.app import App # pylint: disable=ungrouped-imports
@@ -65,11 +64,10 @@ def merge(cls, app_id: str, build_id: str) -> 'Deployment':
6564

6665
async def notify(self) -> None:
6766
app = self.app
68-
channel = app.environment.channel
69-
if not channel:
70-
return None
71-
72-
await ChatNotifier.notify(
73-
app=app,
74-
deployment=self,
75-
)
67+
68+
for channel in app.environment.notification_channels:
69+
engine = channel.build_engine()
70+
await engine.notify(
71+
app=app,
72+
deployment=self,
73+
)

flamingo/models/environment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Environment(Document):
1717
name: str
1818
network: Network = None
1919
project: Project = field(default_factory=Project.default)
20-
channel: NotificationChannel = None
20+
notification_channels: List[NotificationChannel] = field(default_factory=list)
2121
vars: List[EnvVar] = field(default_factory=list)
2222

2323
def __post_init__(self):
Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,38 @@
1-
from dataclasses import dataclass, field
2-
from typing import List
1+
from dataclasses import dataclass
2+
from enum import Enum
3+
from typing import Dict, Union, Any
34

45
from gcp_pilot.datastore import EmbeddedDocument
6+
from sanic_rest.exceptions import ValidationError
7+
8+
from services.notifiers import ChatNotifier, NewRelicNotifier
9+
10+
11+
class NotificationEngine(Enum):
12+
GOOGLE_CHAT = 'google-chat'
13+
NEW_RELIC = 'new-relic'
14+
15+
@classmethod
16+
def get_engine_class(cls, name):
17+
if name == cls.GOOGLE_CHAT.value:
18+
return ChatNotifier
19+
if name == cls.NEW_RELIC.value:
20+
return NewRelicNotifier
21+
raise NotImplementedError(f"Unsupported notification engine {name}")
522

623

724
@dataclass
825
class NotificationChannel(EmbeddedDocument):
9-
webhook_url: str
10-
show_commit_for: List[str] = field(default_factory=lambda: ['SUCCESS'])
26+
engine: str
27+
config: Dict[str, Any]
28+
29+
def __post_init__(self):
30+
# light validation
31+
try:
32+
self.build_engine()
33+
except TypeError as e:
34+
raise ValidationError(str(e)) from e
35+
36+
def build_engine(self) -> Union[ChatNotifier, NewRelicNotifier]:
37+
klass = NotificationEngine.get_engine_class(name=self.engine)
38+
return klass(**self.config)

flamingo/services/new_relic.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from dataclasses import dataclass
2+
from datetime import datetime
3+
from typing import Dict
4+
5+
import requests
6+
7+
8+
NEW_RELIC_API_URL = 'https://api.newrelic.com/v2'
9+
10+
11+
@dataclass
12+
class NewRelicAPI:
13+
api_key: str
14+
15+
def get_app(self, name: str):
16+
resource = 'applications'
17+
params = {'filter[name]': name}
18+
19+
try:
20+
return self._list(resource=resource, params=params)[0]
21+
except IndexError:
22+
return None
23+
24+
def notify_deployment(
25+
self,
26+
app_name: str,
27+
revision: str,
28+
user: str = '',
29+
changelog: str = '',
30+
description: str = '',
31+
timestamp: datetime = None,
32+
):
33+
app_id = self.get_app(name=app_name)['id']
34+
resource = f'applications/{app_id}/deployments'
35+
deployment = dict(revision=revision)
36+
37+
if changelog:
38+
deployment['changelog'] = changelog
39+
if description:
40+
deployment['description'] = description
41+
if timestamp:
42+
deployment['timestamp'] = timestamp.isoformat()
43+
if user:
44+
deployment['user'] = user
45+
46+
payload = {
47+
"deployment": deployment
48+
}
49+
50+
return self._post(
51+
resource=resource,
52+
payload=payload,
53+
)
54+
55+
def _list(self, resource: str, params: Dict = None) -> Dict:
56+
response = requests.get(
57+
url=f'{NEW_RELIC_API_URL}/{resource}.json',
58+
params=params or {},
59+
headers={'X-Api-Key': self.api_key}
60+
)
61+
response.raise_for_status()
62+
return response.json()[resource]
63+
64+
def _post(self, resource: str, payload: Dict = None) -> Dict:
65+
response = requests.post(
66+
url=f'{NEW_RELIC_API_URL}/{resource}.json',
67+
json=payload,
68+
headers={'X-Api-Key': self.api_key}
69+
)
70+
response.raise_for_status()
71+
return response.json()

flamingo/services/notifiers.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,29 @@
11

22

3-
from dataclasses import dataclass
4-
from typing import TYPE_CHECKING, Dict
3+
from dataclasses import dataclass, field
4+
from typing import TYPE_CHECKING, Dict, List
55

66
from gcp_pilot.chats import ChatsHook, Card, Section
77

88
import settings
9+
from services.new_relic import NewRelicAPI
910

1011
if TYPE_CHECKING:
1112
from models.app import App
1213
from models.deployment import Deployment
13-
from models.notification_channel import NotificationChannel
1414

1515

1616
@dataclass
1717
class ChatNotifier:
18-
@classmethod
19-
async def notify(cls, deployment: 'Deployment', app: 'App') -> Dict:
20-
channel = app.environment.channel
18+
webhook_url: str
19+
show_commit_for: List[str] = field(default_factory=lambda: ['SUCCESS'])
2120

22-
chat = ChatsHook(hook_url=channel.webhook_url)
23-
card = cls._build_message_card(deployment=deployment, app=app, channel=channel)
21+
async def notify(self, deployment: 'Deployment', app: 'App') -> Dict:
22+
chat = ChatsHook(hook_url=self.webhook_url)
23+
card = self._build_message_card(deployment=deployment, app=app)
2424
return chat.send_card(card=card, thread_key=f'flamingo_{deployment.build_id}')
2525

26-
@classmethod
27-
def _build_message_card(cls, deployment: 'Deployment', app: 'App', channel: 'NotificationChannel') -> Card:
26+
def _build_message_card(self, deployment: 'Deployment', app: 'App') -> Card:
2827
current_event = deployment.events[-1]
2928
try:
3029
previous_event = deployment.events[-2]
@@ -36,7 +35,7 @@ def _build_message_card(cls, deployment: 'Deployment', app: 'App', channel: 'Not
3635
card = Card()
3736
card.add_header(
3837
title=f'{app.name}',
39-
subtitle=f'{cls._get_action(status=status)} <b>{app.environment_name.upper()}</b>'
38+
subtitle=f'{self._get_action(status=status)} <b>{app.environment_name.upper()}</b>'
4039
)
4140

4241
section = Section()
@@ -57,7 +56,7 @@ def _build_message_card(cls, deployment: 'Deployment', app: 'App', channel: 'Not
5756
content=str(duration),
5857
)
5958

60-
if previous_event and status in channel.show_commit_for:
59+
if previous_event and status in self.show_commit_for:
6160
commits = app.repository.get_commit_diff(
6261
current_revision=current_event.source.revision,
6362
previous_revision=previous_event.source.revision,
@@ -98,3 +97,16 @@ def _get_icon(cls, status: str) -> str:
9897
# https://cloud.google.com/cloud-build/docs/api/reference/rest/v1/projects.builds#status
9998
icon_name = f'deploy_{status.lower()}.png'
10099
return f'https://storage.googleapis.com/{settings.FLAMINGO_GCS_BUCKET}/media/{icon_name}'
100+
101+
102+
@dataclass
103+
class NewRelicNotifier:
104+
api_key: str
105+
106+
async def notify(self, deployment: 'Deployment', app: 'App') -> Dict:
107+
api = NewRelicAPI(api_key=self.api_key)
108+
api.notify_deployment(
109+
app_name=app.identifier,
110+
revision=deployment.build_id,
111+
# user='' # TODO get user
112+
)

0 commit comments

Comments
 (0)