Skip to content

Commit f49a779

Browse files
authored
Merge pull request #5 from actinia-org/webhook
Add endpoint to receive webhook for actinia status update
2 parents 7b5a187 + 30cb8b8 commit f49a779

File tree

13 files changed

+283
-51
lines changed

13 files changed

+283
-51
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
docker

.flake8

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ per-file-ignores =
1010
./tests/cloudevent_receiver_server.py: E501
1111
# remove when implemented:
1212
./src/actinia_cloudevent_plugin/api/cloudevent.py: E501
13-
./src/actinia_cloudevent_plugin/core/processing.py: F841, E501
13+
./src/actinia_cloudevent_plugin/api/hooks.py: F841
14+
./src/actinia_cloudevent_plugin/core/cloudevents.py: F841, E501

.vscode/launch.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"configurations": [
3+
{
4+
"name": "Docker: Python - Flask",
5+
"type": "docker",
6+
"request": "launch",
7+
"preLaunchTask": "docker-run: debug",
8+
"python": {
9+
"pathMappings": [
10+
{
11+
"localRoot": "${workspaceFolder}",
12+
"remoteRoot": "/src/actinia-cloudevent-plugin"
13+
}
14+
],
15+
"projectType": "flask"
16+
},
17+
"dockerServerReadyAction": {
18+
"action": "openExternally",
19+
"pattern": "Running on (https?://\\S+|[0-9]+)",
20+
"uriFormat": "%s://localhost:%s/"
21+
}
22+
}
23+
]
24+
}

.vscode/tasks.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"type": "docker-build",
6+
"label": "docker-build",
7+
"platform": "python",
8+
"dockerBuild": {
9+
"tag": "actinia-cloudevent-plugin:latest",
10+
"dockerfile": "${workspaceFolder}/docker/Dockerfile",
11+
"context": "${workspaceFolder}"
12+
}
13+
},
14+
{
15+
"type": "docker-run",
16+
"label": "docker-run: debug",
17+
"dependsOn": [
18+
"docker-build"
19+
],
20+
"python": {
21+
"module": "flask",
22+
"args": [
23+
"run",
24+
"--no-debugger",
25+
"--host",
26+
"0.0.0.0",
27+
"--port",
28+
"3003"
29+
]
30+
},
31+
"dockerRun": {
32+
"remove": true,
33+
"network": "actinia-docker_actinia-dev",
34+
"ports": [
35+
{
36+
"containerPort": 3003,
37+
"hostPort": 3003
38+
}
39+
],
40+
"env": {
41+
"PYTHONUNBUFFERED": "1",
42+
"PYTHONDONWRITEBYTECODE": "1",
43+
"FLASK_APP": "actinia_cloudevent_plugin.main",
44+
"FLASK_DEBUG": "1",
45+
"FLASK_ENV": "development"
46+
},
47+
"customOptions": "--ip 172.18.0.12",
48+
"volumes": [
49+
{
50+
"localPath": "${workspaceFolder}",
51+
"containerPath": "/src/actinia-cloudevent-plugin",
52+
"permissions": "rw"
53+
}
54+
]
55+
}
56+
}
57+
]
58+
}

docker/Dockerfile

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM alpine:3.21
1+
FROM alpine:3.22
22

33
# python3 + pip3
44
# hadolint ignore=DL3018
@@ -9,23 +9,17 @@ RUN /usr/bin/python -m venv --system-site-packages --without-pip /opt/venv
99
# hadolint ignore=DL3013
1010
RUN python -m ensurepip && pip3 install --no-cache-dir --upgrade pip pep517 wheel
1111

12-
# gunicorn
1312
# hadolint ignore=DL3013
14-
RUN pip3 install --no-cache-dir gunicorn
15-
16-
# needed for tests
17-
# hadolint ignore=DL3013
18-
RUN pip3 install --no-cache-dir setuptools pwgen==0.8.2.post0 pytest==8.3.5 pytest-cov==6.0.0
13+
RUN pip3 install --no-cache-dir gunicorn && \
14+
# needed for tests
15+
pip3 install --no-cache-dir setuptools pwgen==0.8.2.post0 pytest==8.3.5 pytest-cov==6.0.0
1916

2017
COPY . /src/actinia-cloudevent-plugin/
21-
22-
# SETUPTOOLS_SCM_PRETEND_VERSION is only needed if in the plugin folder is no
23-
# .git folder
24-
ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0
18+
RUN pip3 install --no-cache-dir -e /src/actinia-cloudevent-plugin/ && \
19+
# For tests:
20+
chmod a+x /src/actinia-cloudevent-plugin/tests_with_cloudevent_receiver.sh
2521

2622
WORKDIR /src/actinia-cloudevent-plugin
27-
RUN pip3 install --no-cache-dir -e .
28-
29-
# For tests:
30-
RUN chmod a+x tests_with_cloudevent_receiver.sh && make install
3123
# RUN make test
24+
25+
CMD ["gunicorn", "-b", "0.0.0.0:8088", "-w", "8", "--access-logfile=-", "-k", "gthread", "actinia_cloudevent_plugin.main:flask_app"]

docker/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ services:
1111
- SYS_PTRACE
1212
ports:
1313
- "5000:5000"
14-
network_mode: "host"
14+
# network_mode: "host"

src/actinia_cloudevent_plugin/api/cloudevent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from requests.exceptions import ConnectionError # noqa: A004
2828

2929
from actinia_cloudevent_plugin.apidocs import cloudevent
30-
from actinia_cloudevent_plugin.core.processing import (
30+
from actinia_cloudevent_plugin.core.cloudevents import (
3131
cloud_event_to_process_chain,
3232
receive_cloud_event,
3333
send_binary_cloud_event,
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env python
2+
"""Copyright (c) 2025 mundialis GmbH & Co. KG.
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
Hello World class
18+
"""
19+
20+
__license__ = "GPLv3"
21+
__author__ = "Carmen Tawalika"
22+
__copyright__ = "Copyright 2025 mundialis GmbH & Co. KG"
23+
__maintainer__ = "mundialis GmbH & Co. KG"
24+
25+
import json
26+
27+
import requests
28+
from cloudevents.conversion import to_binary
29+
from cloudevents.http import CloudEvent
30+
from flask import jsonify, make_response, request
31+
from flask_restful_swagger_2 import Resource, swagger
32+
33+
from actinia_cloudevent_plugin.apidocs import hooks
34+
from actinia_cloudevent_plugin.model.response_models import (
35+
SimpleStatusCodeResponseModel,
36+
)
37+
from actinia_cloudevent_plugin.resources.config import EVENTRECEIVER
38+
39+
40+
class Hooks(Resource):
41+
"""Webhook handling."""
42+
43+
def get(self, source_name):
44+
"""Cloudevent get method: not allowed response."""
45+
_source_name = source_name
46+
res = jsonify(
47+
SimpleStatusCodeResponseModel(
48+
status=405,
49+
message="Method Not Allowed",
50+
),
51+
)
52+
return make_response(res, 405)
53+
54+
def head(self, source_name):
55+
"""Cloudevent head method: return empty response."""
56+
_source_name = source_name
57+
return make_response("", 200)
58+
59+
@swagger.doc(hooks.describe_hooks_post_docs)
60+
def post(self, source_name) -> SimpleStatusCodeResponseModel:
61+
"""Translate actinia webhook call to cloudevent.
62+
63+
This method is called by HTTP POST actinia-core webhook
64+
"""
65+
# only actinia as source supported so far
66+
if source_name != "actinia":
67+
return make_response(
68+
jsonify(
69+
SimpleStatusCodeResponseModel(
70+
status=400,
71+
message="Bad Request: Source name not 'actinia'",
72+
),
73+
),
74+
400,
75+
)
76+
77+
postbody = request.get_json(force=True)
78+
79+
if type(postbody) is dict:
80+
postbody = json.dumps(postbody)
81+
elif not isinstance(postbody, str):
82+
postbody = str(postbody)
83+
84+
resp = json.loads(postbody)
85+
if "resource_id" not in resp:
86+
return make_response(
87+
jsonify(
88+
SimpleStatusCodeResponseModel(
89+
status=400,
90+
message="Bad Request: No resource_id found in request",
91+
),
92+
),
93+
400,
94+
)
95+
96+
# TODO: define when to send cloudevent
97+
status = resp["status"]
98+
if status == "finished":
99+
# TODO send cloudevent
100+
pass
101+
terminate_status = ["finished", "error", "terminated"]
102+
if status in terminate_status:
103+
# TODO send cloudevent
104+
pass
105+
106+
# TODO: move to common function from core.cloudevents
107+
url = EVENTRECEIVER.url
108+
try:
109+
attributes = {
110+
"specversion": "1.0",
111+
"source": "/actinia-cloudevent-plugin",
112+
"type": "com.mundialis.actinia.process.status",
113+
"subject": "nc_spm_08/PERMANENT",
114+
"datacontenttype": "application/json",
115+
}
116+
data = {"actinia_job": resp}
117+
event = CloudEvent(attributes, data)
118+
headers, body = to_binary(event)
119+
requests.post(url, headers=headers, data=body)
120+
except ConnectionError as e:
121+
return f"Connection ERROR when returning cloudevent: {e}"
122+
except Exception() as e:
123+
return f"ERROR when returning cloudevent: {e}"
124+
125+
res = jsonify(
126+
SimpleStatusCodeResponseModel(
127+
status=200,
128+
message="Thank you for your update",
129+
),
130+
)
131+
return make_response(res, 200)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python
2+
"""Copyright (c) 2025 mundialis GmbH & Co. KG.
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
Apidocs for webhook endpoint
18+
"""
19+
20+
__license__ = "GPLv3"
21+
__author__ = "Carmen Tawalika"
22+
__copyright__ = "Copyright 2025 mundialis GmbH & Co. KG"
23+
__maintainer__ = "mundialis GmbH & Co. KG"
24+
25+
26+
from actinia_cloudevent_plugin.model.response_models import (
27+
SimpleStatusCodeResponseModel,
28+
)
29+
30+
describe_hooks_post_docs = {
31+
# "summary" is taken from the description of the get method
32+
"tags": ["cloudevent"],
33+
"description": (
34+
"Receives webhook with status update e.g. from actinia-core,"
35+
" transforms to cloudevent and sends it to configurable endpoint."
36+
),
37+
"responses": {
38+
"200": {
39+
"description": (
40+
"This response returns a cloud event, "
41+
"generated from actinia-core status"
42+
),
43+
"schema": SimpleStatusCodeResponseModel,
44+
},
45+
"400": {
46+
"description": "This response returns an error message",
47+
"schema": SimpleStatusCodeResponseModel,
48+
},
49+
},
50+
}
File renamed without changes.

0 commit comments

Comments
 (0)