Skip to content

Commit d04c8e5

Browse files
Merge branch 'master' into abhsingh
2 parents 18ec44a + da033fb commit d04c8e5

File tree

6 files changed

+156
-1
lines changed

6 files changed

+156
-1
lines changed

elementary/clients/s3/client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ def send_report(
4141
bucket_report_path = remote_bucket_file_path or report_filename
4242
bucket_website_url = None
4343
logger.info(f'Uploading to S3 bucket "{self.config.s3_bucket_name}"')
44+
45+
extra_args = {"ContentType": "text/html"}
46+
if self.config.s3_acl is not None:
47+
extra_args["ACL"] = self.config.s3_acl
4448
self.client.upload_file(
4549
local_html_file_path,
4650
self.config.s3_bucket_name,
4751
bucket_report_path,
48-
ExtraArgs={"ContentType": "text/html"},
52+
ExtraArgs=extra_args,
4953
)
5054
logger.info("Uploaded report to S3.")
5155
if self.config.update_bucket_website:

elementary/config/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def __init__(
6262
aws_session_token: Optional[str] = None,
6363
s3_endpoint_url: Optional[str] = None,
6464
s3_bucket_name: Optional[str] = None,
65+
s3_acl: Optional[str] = None,
6566
google_project_name: Optional[str] = None,
6667
google_service_account_path: Optional[str] = None,
6768
gcs_bucket_name: Optional[str] = None,
@@ -159,6 +160,7 @@ def __init__(
159160
self.aws_access_key_id = aws_access_key_id
160161
self.aws_secret_access_key = aws_secret_access_key
161162
self.aws_session_token = aws_session_token
163+
self.s3_acl = s3_acl
162164

163165
google_config = config.get(self._GOOGLE, {})
164166
self.google_project_name = self._first_not_none(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .file_system import FileSystemMessagingIntegration
2+
from .slack_webhook import SlackWebhookMessagingIntegration
3+
from .teams_webhook import TeamsWebhookMessagingIntegration
4+
5+
__all__ = [
6+
"FileSystemMessagingIntegration",
7+
"TeamsWebhookMessagingIntegration",
8+
"SlackWebhookMessagingIntegration",
9+
]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import json
2+
from datetime import datetime
3+
from pathlib import Path
4+
5+
from elementary.messages.message_body import MessageBody
6+
from elementary.messages.messaging_integrations.base_messaging_integration import (
7+
BaseMessagingIntegration,
8+
MessageSendResult,
9+
)
10+
from elementary.messages.messaging_integrations.empty_message_context import (
11+
EmptyMessageContext,
12+
)
13+
from elementary.messages.messaging_integrations.exceptions import (
14+
MessagingIntegrationError,
15+
)
16+
from elementary.utils.log import get_logger
17+
18+
logger = get_logger(__name__)
19+
20+
21+
class FileSystemMessagingIntegration(
22+
BaseMessagingIntegration[str, EmptyMessageContext]
23+
):
24+
def __init__(self, directory: str, create_if_missing: bool = True) -> None:
25+
self.directory = Path(directory).expanduser().resolve()
26+
self._create_if_missing = create_if_missing
27+
28+
if not self.directory.exists():
29+
if self._create_if_missing:
30+
logger.info(
31+
f"Creating directory for FileSystemMessagingIntegration: {self.directory}"
32+
)
33+
self.directory.mkdir(parents=True, exist_ok=True)
34+
else:
35+
raise MessagingIntegrationError(
36+
f"Directory {self.directory} does not exist and create_if_missing is False"
37+
)
38+
39+
def supports_reply(self) -> bool:
40+
return False
41+
42+
def send_message(
43+
self, destination: str, body: MessageBody
44+
) -> MessageSendResult[EmptyMessageContext]:
45+
channel_dir = self.directory / destination
46+
if not channel_dir.exists():
47+
if self._create_if_missing:
48+
channel_dir.mkdir(parents=True, exist_ok=True)
49+
else:
50+
raise MessagingIntegrationError(
51+
f"Channel directory {channel_dir} does not exist and create_if_missing is False"
52+
)
53+
54+
filename = datetime.utcnow().strftime("%Y%m%dT%H%M%S_%fZ.json")
55+
file_path = channel_dir / filename
56+
57+
try:
58+
json_str = json.dumps(body.dict(), indent=2)
59+
file_path.write_text(json_str, encoding="utf-8")
60+
except Exception as exc:
61+
logger.error(
62+
f"Failed to write alert message to file {file_path}: {exc}",
63+
exc_info=True,
64+
)
65+
raise MessagingIntegrationError(
66+
f"Failed writing alert message to file {file_path}"
67+
) from exc
68+
69+
return MessageSendResult(
70+
timestamp=datetime.utcnow(),
71+
message_format="json",
72+
message_context=EmptyMessageContext(),
73+
)

elementary/monitor/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,12 @@ def report(
528528
default=None,
529529
help="The name of the S3 bucket to upload the report to.",
530530
)
531+
@click.option(
532+
"--s3-acl",
533+
type=str,
534+
default=None,
535+
help="S3 Canned ACL value used to modify report permissions, for example set to 'public-read' to make the report publicly accessible.",
536+
)
531537
@click.option(
532538
"--google-service-account-path",
533539
type=str,
@@ -640,6 +646,7 @@ def send_report(
640646
aws_session_token,
641647
s3_endpoint_url,
642648
s3_bucket_name,
649+
s3_acl,
643650
azure_connection_string,
644651
azure_container_name,
645652
google_service_account_path,
@@ -688,6 +695,7 @@ def send_report(
688695
azure_container_name=azure_container_name,
689696
s3_endpoint_url=s3_endpoint_url,
690697
s3_bucket_name=s3_bucket_name,
698+
s3_acl=s3_acl,
691699
google_service_account_path=google_service_account_path,
692700
google_project_name=google_project_name,
693701
gcs_bucket_name=gcs_bucket_name,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import json
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from elementary.messages.blocks import LineBlock, LinesBlock, TextBlock
7+
from elementary.messages.message_body import MessageBody
8+
from elementary.messages.messaging_integrations.exceptions import (
9+
MessagingIntegrationError,
10+
)
11+
from elementary.messages.messaging_integrations.file_system import (
12+
FileSystemMessagingIntegration,
13+
)
14+
15+
16+
def _build_body() -> MessageBody:
17+
return MessageBody(
18+
blocks=[LinesBlock(lines=[LineBlock(inlines=[TextBlock(text="hello")])])]
19+
)
20+
21+
22+
def test_send_message_creates_file_in_channel_dir(tmp_path: Path) -> None:
23+
root_dir = tmp_path / "alerts"
24+
integ = FileSystemMessagingIntegration(directory=str(root_dir))
25+
body = _build_body()
26+
27+
integ.send_message("channel", body)
28+
29+
channel_dir = root_dir / "channel"
30+
files = list(channel_dir.glob("*.json"))
31+
assert len(files) == 1, "Expected exactly one file in the channel directory"
32+
33+
message_json = files[0].read_text()
34+
assert json.loads(message_json) == json.loads(body.json())
35+
36+
37+
def test_send_multiple_messages_creates_multiple_files(tmp_path: Path) -> None:
38+
root_dir = tmp_path / "alerts"
39+
integ = FileSystemMessagingIntegration(directory=str(root_dir))
40+
body1 = _build_body()
41+
body2 = _build_body()
42+
43+
integ.send_message("channel", body1)
44+
integ.send_message("channel", body2)
45+
46+
channel_dir = root_dir / "channel"
47+
files = sorted(channel_dir.glob("*.json"))
48+
49+
assert len(files) == 2
50+
assert json.loads(files[0].read_text()) == json.loads(body1.json())
51+
assert json.loads(files[1].read_text()) == json.loads(body2.json())
52+
53+
54+
def test_send_message_no_create_flag(tmp_path: Path) -> None:
55+
directory = tmp_path / "alerts-no-create"
56+
with pytest.raises(MessagingIntegrationError):
57+
FileSystemMessagingIntegration(
58+
directory=str(directory), create_if_missing=False
59+
)

0 commit comments

Comments
 (0)