Skip to content

Commit cf79cc8

Browse files
committed
Add notify to slack / telegram
1 parent 4fff979 commit cf79cc8

8 files changed

Lines changed: 378 additions & 5 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,7 @@ ENV/
102102

103103
# docker/helm
104104
docker/certificates/**/*
105-
rules.py
105+
rules.py
106+
107+
# config file
108+
artifactory-cleanup.yaml

artifactory_cleanup/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ def register(rule):
77
registry.register(rule)
88

99

10-
__version__ = "1.0.18"
10+
__version__ = "1.1.0"

artifactory_cleanup/cli.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
)
1616
from artifactory_cleanup.base_url_session import BaseUrlSession
1717
from artifactory_cleanup.context_managers import get_context_managers
18-
from artifactory_cleanup.errors import InvalidConfigError
18+
from artifactory_cleanup.errors import InvalidConfigError, NotificationError
1919
from artifactory_cleanup.loaders import (
2020
PythonLoader,
2121
YamlConfigLoader,
2222
)
23+
from artifactory_cleanup.notifiers import ReportNotifier, SlackConfig, TelegramConfig
2324

2425
requests.packages.urllib3.disable_warnings()
2526

@@ -102,6 +103,44 @@ class ArtifactoryCleanupCLI(cli.Application):
102103
default=False,
103104
)
104105

106+
_slack_token = cli.SwitchAttr(
107+
"--slack-token",
108+
help="Slack bot token for report upload",
109+
mandatory=False,
110+
envname="ARTIFACTORY_CLEANUP_SLACK_TOKEN",
111+
requires=["--slack-channel-id", "--output"],
112+
)
113+
114+
_slack_channel_id = cli.SwitchAttr(
115+
"--slack-channel-id",
116+
help="Slack channel id for report upload",
117+
mandatory=False,
118+
envname="ARTIFACTORY_CLEANUP_SLACK_CHANNEL_ID",
119+
requires=["--slack-token", "--output"],
120+
)
121+
122+
_telegram_token = cli.SwitchAttr(
123+
"--telegram-token",
124+
help="Telegram bot token for report upload",
125+
mandatory=False,
126+
envname="ARTIFACTORY_CLEANUP_TELEGRAM_TOKEN",
127+
requires=["--telegram-chat-id", "--output"],
128+
)
129+
130+
_telegram_chat_id = cli.SwitchAttr(
131+
"--telegram-chat-id",
132+
help="Telegram chat id for report upload",
133+
mandatory=False,
134+
envname="ARTIFACTORY_CLEANUP_TELEGRAM_CHAT_ID",
135+
requires=["--telegram-token", "--output"],
136+
)
137+
138+
_notify_comment = cli.SwitchAttr(
139+
"--notify-comment",
140+
help="Comment attached to uploaded report",
141+
mandatory=False,
142+
)
143+
105144
@property
106145
def VERSION(self):
107146
# To prevent circular imports
@@ -153,6 +192,36 @@ def _create_output_file(self, result, filename, format):
153192
with open(filename, "w", encoding="utf-8") as file:
154193
file.write(text)
155194

195+
def _has_notifiers(self) -> bool:
196+
return any(
197+
[
198+
self._slack_token,
199+
self._slack_channel_id,
200+
self._telegram_token,
201+
self._telegram_chat_id,
202+
]
203+
)
204+
205+
def _build_notifier(self) -> ReportNotifier:
206+
slack = None
207+
if self._slack_token and self._slack_channel_id:
208+
slack = SlackConfig(
209+
token=self._slack_token, channel_id=self._slack_channel_id
210+
)
211+
telegram = None
212+
if self._telegram_token and self._telegram_chat_id:
213+
telegram = TelegramConfig(
214+
token=self._telegram_token, chat_id=self._telegram_chat_id
215+
)
216+
return ReportNotifier(slack=slack, telegram=telegram)
217+
218+
def _apply_notifier_config(self, config):
219+
self._slack_token = self._slack_token or config.get("slack_token", "")
220+
self._slack_channel_id = self._slack_channel_id or config.get("slack_channel_id", "")
221+
self._telegram_token = self._telegram_token or config.get("telegram_token", "")
222+
self._telegram_chat_id = self._telegram_chat_id or config.get("telegram_chat_id", "")
223+
self._notify_comment = self._notify_comment or config.get("notify_comment", "")
224+
156225
def main(self):
157226
today = self._get_today()
158227
if self._load_rules:
@@ -166,6 +235,8 @@ def main(self):
166235
print(str(err), file=sys.stderr)
167236
sys.exit(1)
168237

238+
self._apply_notifier_config(loader.get_notifications())
239+
169240
server, user, password, apikey = loader.get_connection()
170241
session = BaseUrlSession(server)
171242
if apikey:
@@ -218,6 +289,13 @@ def main(self):
218289

219290
if self._output_file:
220291
self._create_output_file(result, self._output_file, self._output_format)
292+
if self._has_notifiers():
293+
try:
294+
notifier = self._build_notifier()
295+
notifier.send_file(self._output_file, comment=self._notify_comment)
296+
except NotificationError as err:
297+
print(str(err), file=sys.stderr)
298+
sys.exit(1)
221299

222300

223301
if __name__ == "__main__":

artifactory_cleanup/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ class ArtifactoryCleanupException(Exception):
44

55
class InvalidConfigError(ArtifactoryCleanupException):
66
pass
7+
8+
9+
class NotificationError(ArtifactoryCleanupException):
10+
pass

artifactory_cleanup/loaders.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,29 @@ def get_root_schema(self, rules):
9494
config_schema = cfgv.Map(
9595
"Config",
9696
None,
97-
cfgv.NoAdditionalKeys(["server", "user", "password", "policies", "apikey"]),
97+
cfgv.NoAdditionalKeys(
98+
[
99+
"server",
100+
"user",
101+
"password",
102+
"policies",
103+
"apikey",
104+
"slack_token",
105+
"slack_channel_id",
106+
"telegram_token",
107+
"telegram_chat_id",
108+
"notify_comment",
109+
]
110+
),
98111
cfgv.Required("server", cfgv.check_string),
99112
# User and password required, if apikey missing
100113
cfgv.Conditional("user", cfgv.check_string, "apikey", cfgv.MISSING, False),
101114
cfgv.Conditional("password", cfgv.check_string, "apikey", cfgv.MISSING, False),
115+
cfgv.Optional("slack_token", cfgv.check_string, ""),
116+
cfgv.Optional("slack_channel_id", cfgv.check_string, ""),
117+
cfgv.Optional("telegram_token", cfgv.check_string, ""),
118+
cfgv.Optional("telegram_chat_id", cfgv.check_string, ""),
119+
cfgv.Optional("notify_comment", cfgv.check_string, ""),
102120
cfgv.RequiredRecurse("policies", cfgv.Array(policy_schema)),
103121
)
104122

@@ -198,6 +216,21 @@ def get_connection(self) -> Tuple[str, str, str, str]:
198216
apikey = os.path.expandvars(apikey)
199217
return server, user, password, apikey
200218

219+
def get_notifications(self) -> Dict[str, str]:
220+
config = self.load(self.filepath)
221+
notifications = config.get("artifactory-cleanup", {})
222+
return {
223+
"slack_token": os.path.expandvars(notifications.get("slack_token", "")),
224+
"slack_channel_id": os.path.expandvars(
225+
notifications.get("slack_channel_id", "")
226+
),
227+
"telegram_token": os.path.expandvars(notifications.get("telegram_token", "")),
228+
"telegram_chat_id": os.path.expandvars(
229+
notifications.get("telegram_chat_id", "")
230+
),
231+
"notify_comment": os.path.expandvars(notifications.get("notify_comment", "")),
232+
}
233+
201234

202235
class PythonLoader:
203236
"""

artifactory_cleanup/notifiers.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import os
2+
from dataclasses import dataclass
3+
from typing import Optional, List
4+
5+
import requests
6+
7+
from artifactory_cleanup.errors import NotificationError
8+
9+
10+
@dataclass
11+
class SlackConfig:
12+
token: str
13+
channel_id: str
14+
15+
16+
@dataclass
17+
class TelegramConfig:
18+
token: str
19+
chat_id: str
20+
21+
22+
class SlackNotifier:
23+
def __init__(self, config: SlackConfig):
24+
self._config = config
25+
26+
def _headers(self):
27+
return {"Authorization": f"Bearer {self._config.token}"}
28+
29+
def send_file(self, filename: str, comment: Optional[str] = None):
30+
if not os.path.exists(filename):
31+
raise NotificationError(f"Report file not found: {filename}")
32+
33+
file_size = os.path.getsize(filename)
34+
file_basename = os.path.basename(filename)
35+
metadata_response = requests.post(
36+
"https://slack.com/api/files.getUploadURLExternal",
37+
headers=self._headers(),
38+
data={
39+
"filename": file_basename,
40+
"length": file_size,
41+
},
42+
timeout=30,
43+
)
44+
metadata_response.raise_for_status()
45+
metadata = metadata_response.json()
46+
if not metadata.get("ok"):
47+
raise NotificationError(
48+
f"Slack upload URL request failed: {metadata.get('error', 'unknown error')}"
49+
)
50+
51+
upload_url = metadata["upload_url"]
52+
file_id = metadata["file_id"]
53+
54+
with open(filename, "rb") as report_file:
55+
upload_response = requests.post(
56+
upload_url,
57+
data=report_file,
58+
headers={"Content-Type": "application/octet-stream"},
59+
timeout=60,
60+
)
61+
upload_response.raise_for_status()
62+
63+
complete_payload = {
64+
"files": [{"id": file_id, "title": file_basename}],
65+
"channel_id": self._config.channel_id,
66+
}
67+
if comment:
68+
complete_payload["initial_comment"] = comment
69+
70+
complete_response = requests.post(
71+
"https://slack.com/api/files.completeUploadExternal",
72+
headers={
73+
**self._headers(),
74+
"Content-Type": "application/json; charset=utf-8",
75+
},
76+
json=complete_payload,
77+
timeout=30,
78+
)
79+
complete_response.raise_for_status()
80+
complete = complete_response.json()
81+
if not complete.get("ok"):
82+
raise NotificationError(
83+
f"Slack upload finalize failed: {complete.get('error', 'unknown error')}"
84+
)
85+
86+
87+
class TelegramNotifier:
88+
def __init__(self, config: TelegramConfig):
89+
self._config = config
90+
91+
def send_file(self, filename: str, comment: Optional[str] = None):
92+
if not os.path.exists(filename):
93+
raise NotificationError(f"Report file not found: {filename}")
94+
95+
url = f"https://api.telegram.org/bot{self._config.token}/sendDocument"
96+
data = {"chat_id": self._config.chat_id}
97+
if comment:
98+
data["caption"] = comment
99+
100+
with open(filename, "rb") as report_file:
101+
response = requests.post(
102+
url,
103+
data=data,
104+
files={"document": report_file},
105+
timeout=60,
106+
)
107+
response.raise_for_status()
108+
payload = response.json()
109+
if not payload.get("ok"):
110+
raise NotificationError(
111+
f"Telegram send failed: {payload.get('description', 'unknown error')}"
112+
)
113+
114+
115+
class ReportNotifier:
116+
def __init__(
117+
self,
118+
slack: Optional[SlackConfig] = None,
119+
telegram: Optional[TelegramConfig] = None,
120+
):
121+
self._notifiers: List = []
122+
if slack:
123+
self._notifiers.append(SlackNotifier(slack))
124+
if telegram:
125+
self._notifiers.append(TelegramNotifier(telegram))
126+
127+
def send_file(self, filename: str, comment: Optional[str] = None):
128+
for notifier in self._notifiers:
129+
notifier.send_file(filename=filename, comment=comment)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
setup(
1414
name="artifactory-cleanup",
15-
version="1.0.18",
15+
version="1.1.0",
1616
description="Rules and cleanup policies for Artifactory",
1717
long_description=long_description,
1818
long_description_content_type="text/markdown",

0 commit comments

Comments
 (0)