-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from chhsiao1981/notification
notification
- Loading branch information
Showing
5 changed files
with
396 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# Continuous integration testing for ChRIS Plugin. | ||
# https://github.com/FNNDSC/python-chrisapp-template/wiki/Continuous-Integration | ||
# | ||
# - on push and PR: run pytest | ||
# - on push to main: build and push container images as ":latest" | ||
# - on push to semver tag: build and push container image with tag and | ||
# upload plugin description to https://chrisstore.co | ||
|
||
name: build | ||
|
||
on: | ||
push: | ||
branches: [ main ] | ||
tags: | ||
- "v?[0-9]+.[0-9]+.[0-9]+*" | ||
pull_request: | ||
branches: [ main ] | ||
|
||
jobs: | ||
build: | ||
name: Build | ||
if: github.event_name == 'push' || github.event_name == 'release' | ||
runs-on: ubuntu-22.04 | ||
|
||
steps: | ||
- name: Decide image tags | ||
id: info | ||
shell: python | ||
run: | | ||
import os | ||
import itertools | ||
def join_tag(t): | ||
registry, repo, tag = t | ||
return f'{registry}/{repo}:{tag}'.lower() | ||
registries = ['docker.io', 'ghcr.io'] | ||
repos = ['${{ github.repository }}'] | ||
if '${{ github.ref_type }}' == 'branch': | ||
tags = ['latest'] | ||
elif '${{ github.ref_type }}' == 'tag': | ||
tag = '${{ github.ref_name }}' | ||
version = tag[1:] if tag.startswith('v') else tag | ||
tags = ['latest', version] | ||
else: | ||
tags = [] | ||
if '${{ github.ref_type }}' == 'tag': | ||
local_tag = join_tag(('ghcr.io', '${{ github.repository }}', version)) | ||
else: | ||
local_tag = join_tag(('localhost', '${{ github.repository }}', 'latest')) | ||
product = itertools.product(registries, repos, tags) | ||
tags_csv = ','.join(map(join_tag, product)) | ||
outputs = { | ||
'tags_csv' : tags_csv, | ||
'push' : 'true' if tags_csv else 'false', | ||
'local_tag': local_tag | ||
} | ||
with open(os.environ['GITHUB_OUTPUT'], 'a') as out: | ||
for k, v in outputs.items(): | ||
out.write(f'{k}={v}\n') | ||
- uses: actions/checkout@v3 | ||
# QEMU is used for non-x86_64 builds | ||
- uses: docker/setup-qemu-action@v3 | ||
# buildx adds additional features to docker build | ||
- uses: docker/setup-buildx-action@v3 | ||
with: | ||
driver-opts: network=host | ||
|
||
# Here, we want to do the docker build twice: | ||
# The first build pushes to our local registry for testing. | ||
# The second build pushes to Docker Hub and ghcr.io | ||
- name: Build (local only) | ||
uses: docker/build-push-action@v3 | ||
id: docker_build | ||
with: | ||
context: . | ||
file: ./Dockerfile | ||
tags: ${{ steps.info.outputs.local_tag }} | ||
load: true | ||
cache-from: type=gha | ||
- name: Login to DockerHub | ||
if: (github.event_name == 'push' || github.event_name == 'release') && contains(steps.info.outputs.tags_csv, 'docker.io') | ||
uses: docker/login-action@v3 | ||
with: | ||
username: ${{ secrets.DOCKERHUB_USERNAME }} | ||
password: ${{ secrets.DOCKERHUB_PASSWORD }} | ||
- name: Login to GitHub Container Registry | ||
if: (github.event_name == 'push' || github.event_name == 'release') && contains(steps.info.outputs.tags_csv, 'ghcr.io') | ||
uses: docker/login-action@v3 | ||
with: | ||
registry: ghcr.io | ||
username: ${{ github.repository_owner }} | ||
password: ${{ secrets.GITHUB_TOKEN }} | ||
- name: Build and push | ||
uses: docker/build-push-action@v3 | ||
if: (github.event_name == 'push' || github.event_name == 'release') | ||
with: | ||
context: . | ||
file: ./Dockerfile | ||
tags: ${{ steps.info.outputs.tags_csv }} | ||
# linux/ppc64le not working, see https://github.com/ANTsX/ANTs/issues/1644 | ||
platforms: linux/amd64,linux/arm64 | ||
push: ${{ steps.info.outputs.push }} | ||
cache-to: type=gha,mode=max | ||
|
||
- name: Upload ChRIS Plugin | ||
id: upload | ||
if: github.ref_type == 'tag' | ||
uses: FNNDSC/upload-chris-plugin@v1 | ||
with: | ||
dock_image: ${{ steps.info.outputs.local_tag }} | ||
username: ${{ secrets.CHRISPROJECT_USERNAME }} | ||
password: ${{ secrets.CHRISPROJECT_PASSWORD }} | ||
chris_url: https://cube.chrisproject.org/api/v1/ | ||
compute_names: host,galena | ||
|
||
- name: Update DockerHub description | ||
if: steps.upload.outcome == 'success' | ||
uses: peter-evans/dockerhub-description@v3 | ||
continue-on-error: true # it is not crucial that this works | ||
with: | ||
username: ${{ secrets.DOCKERHUB_USERNAME }} | ||
password: ${{ secrets.DOCKERHUB_PASSWORD }} | ||
short-description: ${{ steps.upload.outputs.title }} | ||
readme-filepath: ./README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Python version can be changed, e.g. | ||
FROM docker.io/python:3.11.3-slim-bullseye | ||
|
||
LABEL org.opencontainers.image.authors="FNNDSC <dev@babyMRI.org>" \ | ||
org.opencontainers.image.title="An email plugin" \ | ||
org.opencontainers.image.description="A ChRIS Plugin for notification through mail / Slack / Element" | ||
|
||
ARG SRCDIR=/usr/local/src/pl-notification | ||
RUN mkdir -p ${SRCDIR} | ||
WORKDIR ${SRCDIR} | ||
|
||
COPY . . | ||
RUN pip install "." \ | ||
&& cd / | ||
WORKDIR / | ||
|
||
CMD ["notification"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
#!/usr/bin/env python | ||
|
||
from pathlib import Path | ||
from argparse import ArgumentParser, Namespace | ||
|
||
import os | ||
import os.path | ||
|
||
import yaml | ||
import json | ||
|
||
import smtplib | ||
import requests | ||
from email.message import EmailMessage | ||
import urllib | ||
|
||
import time | ||
|
||
from chris_plugin import chris_plugin | ||
from pflog import pflog | ||
|
||
# XXX Make sure that your cfg filename starts with '.' | ||
_CFG_FILENAME = '.notification.yaml' | ||
|
||
_DEFAULT_ELEMENT_HOST = 'fedora.ems.host' | ||
|
||
|
||
parser = ArgumentParser(description='A ChRIS Plugin for notification through mail / Slack / Element') | ||
parser.add_argument('-c', '--content', type=str, required=False, default='', help=f'Content of the notification. Optionally specified in [inputdir]/{_CFG_FILENAME} if not specified in argument.') | ||
|
||
parser.add_argument('-t', '--title', type=str, required=False, default='', help=f'[Optional] Title of the notification. Optionally specified in [inputdir]/{_CFG_FILENAME} if not specified in argument.') | ||
|
||
parser.add_argument('-s', '--slack-url', type=str, required=False, default='', | ||
help=f'[Optional] slack-url if we want to send notification through Slack. Optionally specified in [inputdir]/{_CFG_FILENAME} if not specified in argument.') | ||
|
||
parser.add_argument('-e', '--element-room', type=str, required=False, default='', | ||
help=f'[Optional] element-room (ex: ![room-id]:fedora.im) if we want to send notification through Element. Optionally specified in [inputdir]/{_CFG_FILENAME} if not specified in argument.') | ||
|
||
parser.add_argument('-E', '--element-token', type=str, required=False, default='', | ||
help=f'[Optional] element-token if we want to send notification through Element. Required if element-url exists. Optionally specified in [inputdir]/{_CFG_FILENAME} if not specified in argument.') | ||
|
||
parser.add_argument('--element-host', type=str, required=False, default=f'{_DEFAULT_ELEMENT_HOST}', | ||
help=f'[Optional] element-host if we want to send notification through Element. [inputdir]/{_CFG_FILENAME} is with higher priority than command-line based config.') | ||
|
||
|
||
parser.add_argument('-r', '--rcpt', type=str, required=False, default='', | ||
help=f'[Optional] comma separated email receipients if we want to send notification through email. Optionally specified in [inputdir]/{_CFG_FILENAME} if not specified in argument.') | ||
|
||
parser.add_argument('-S', '--sender', type=str, required=False, default='noreply@fnndsc.org', help=f'sender (From) in email. [inputdir]/{_CFG_FILENAME} is with higher priority than command-line based config.') | ||
parser.add_argument('-M', '--mail-server', type=str, required=False, default='postfix.postfix.svc.k8s.galena.fnndsc', help=f'email server. [inputdir]/{_CFG_FILENAME} is with higher priority than command-line based config.') | ||
|
||
|
||
@chris_plugin( | ||
parser=parser, | ||
title='A ChRIS plugin for notification through mail / Slack / Element', | ||
category='', # ref. https://chrisstore.co/plugins | ||
min_memory_limit='500Mi', # supported units: Mi, Gi | ||
min_cpu_limit='1000m', # millicores, e.g. "1000m" = 1 CPU core | ||
min_gpu_limit=0, # set min_gpu_limit=1 to enable GPU | ||
) | ||
@pflog.tel_logTime( | ||
event='notification', | ||
log='Notification', | ||
) | ||
def main(options: Namespace, inputdir: Path, outputdir: Path): | ||
inputdir_str = str(inputdir) | ||
outputdir_str = str(outputdir) | ||
print(f'inputdir: {inputdir_str} outputdir: {outputdir_str}') | ||
|
||
yaml_cfg = {} | ||
yaml_filename = os.sep.join([inputdir_str, _CFG_FILENAME]) | ||
if os.path.exists(yaml_filename): | ||
with open(yaml_filename, 'r') as f: | ||
yaml_cfg = yaml.full_load(f) | ||
|
||
content = _cfg_or_arg(options.content, yaml_cfg, 'content', f'Notification: not in [inputdir]/{_CFG_FILENAME} and no --content') | ||
|
||
title = _arg_or_cfg(options.title, yaml_cfg, 'title') | ||
|
||
slack_url = _arg_or_cfg(options.slack_url, yaml_cfg, 'slack-url') | ||
|
||
element_room = _arg_or_cfg(options.element_room, yaml_cfg, 'element-room') | ||
element_token = _arg_or_cfg(options.element_token, yaml_cfg, 'element-token') | ||
element_host = _cfg_or_arg(options.element_host, yaml_cfg, 'element-host') | ||
|
||
rcpt = _arg_or_cfg(options.rcpt, yaml_cfg, 'rcpt') | ||
sender = _cfg_or_arg(options.sender, yaml_cfg, 'sender') | ||
mail_server = _cfg_or_arg(options.mail_server, yaml_cfg, 'mail-server') | ||
|
||
if slack_url: | ||
_send_slack(title=title, content=content, url=slack_url) | ||
|
||
if element_room and element_token: | ||
_send_element(title=title, content=content, room=element_room, token=element_token, host=element_host) | ||
|
||
if rcpt: | ||
_send_email(title=title, content=content, rcpt=rcpt, mail_server=mail_server, sender=sender) | ||
|
||
|
||
def _arg_or_cfg(arg_val: str, cfg: dict, index: str, err_msg: str = '') -> str: | ||
if arg_val: | ||
return arg_val | ||
|
||
cfg_val = cfg.get(index, '') | ||
if cfg_val: | ||
return cfg_val | ||
|
||
if not err_msg: | ||
return '' | ||
|
||
raise RuntimeError(err_msg) | ||
|
||
|
||
def _cfg_or_arg(arg_val: str, cfg: dict, index: str, err_msg: str = '') -> str: | ||
cfg_val = cfg.get(index, '') | ||
if cfg_val: | ||
return cfg_val | ||
|
||
if arg_val: | ||
return arg_val | ||
|
||
if not err_msg: | ||
return '' | ||
|
||
raise RuntimeError(err_msg) | ||
|
||
|
||
def _send_slack(title: str, content: str, url: str): | ||
text_list = [] | ||
if title: | ||
text_list.append(f'*{title}*') | ||
if content: | ||
text_list.append(content) | ||
text = '\n'.join(text_list) | ||
the_data = { | ||
'text': text, | ||
} | ||
|
||
headers = { | ||
'Content-Type': 'application/json', | ||
} | ||
|
||
resp = requests.post(url, data=json.dumps(the_data), headers=headers) | ||
if resp.status_code == 200: | ||
return | ||
|
||
raise resp.raise_for_status() | ||
|
||
|
||
def _send_element(title: str, content: str, room: str, token: str, host=f'{_DEFAULT_ELEMENT_HOST}'): | ||
if not host: | ||
host = _DEFAULT_ELEMENT_HOST | ||
|
||
formatted_text_list = [] | ||
text_list = [] | ||
if title: | ||
formatted_text_list.append(f'<h6>[BOT]{title}</h6>') | ||
text_list.append(f'[BOT][{title}]') | ||
else: | ||
formatted_text_list.append('<h6>[BOT]</h6>') | ||
text_list.append('[BOT]') | ||
|
||
if content: | ||
formatted_text_list.append(content) | ||
text_list.append(content) | ||
|
||
formatted_text = ''.join(formatted_text_list) | ||
text = ' '.join(text_list) | ||
|
||
the_data = { | ||
'formatted_body': formatted_text, | ||
'body': text, | ||
'msgtype': 'm.text', | ||
'format': 'org.matrix.custom.html', | ||
} | ||
|
||
headers = { | ||
'Content-Type': 'application/json', | ||
'Accept': 'application/json', | ||
'Access-Control-Allow-Origin': '*', | ||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', | ||
'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type, Authorization', | ||
} | ||
|
||
txn_id = 'chhsiaobot' + str(time.time() * 1000) | ||
percent_room = _parse_element_room(room) | ||
url = f'https://{host}/_matrix/client/v3/rooms/{percent_room}/send/m.room.message/{txn_id}?access_token={token}' | ||
|
||
resp = requests.put(url, data=json.dumps(the_data), headers=headers) | ||
if resp.status_code == 200: | ||
return | ||
|
||
raise resp.raise_for_status() | ||
|
||
|
||
def _parse_element_room(room: str) -> str: | ||
if room.startswith('%21'): | ||
return room | ||
|
||
if not room.startswith('!'): | ||
room = '!' + room | ||
|
||
return urllib.parse.quote(room, safe='/', encoding=None, errors=None) | ||
|
||
|
||
def _send_email(title: str, content: str, rcpt: str, mail_server: str, sender: str): | ||
if not rcpt: | ||
raise RuntimeError(f'Email: no --rcpt and not in [inputdir]/{_CFG_FILENAME}') | ||
|
||
msg = EmailMessage() | ||
msg['Subject'] = title | ||
msg['From'] = sender | ||
msg['To'] = rcpt | ||
msg.set_content(content) | ||
|
||
s = smtplib.SMTP(mail_server) | ||
s.send_message(msg) | ||
s.quit() | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
title: '' | ||
content: '' | ||
|
||
slack-url: '' | ||
|
||
element-room: '' | ||
element-token: '' | ||
element-host: '' | ||
|
||
rcpt: '' | ||
sender: '' | ||
mailserver: '' |
Oops, something went wrong.