Skip to content

Commit f44f406

Browse files
author
ryantiffany
committed
Adding in webhook function
1 parent a3448e5 commit f44f406

File tree

6 files changed

+346
-0
lines changed

6 files changed

+346
-0
lines changed

diagrams/ce-fn-webhook.drawio

Lines changed: 165 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Github webhook function
2+
3+
![Workflow diagram](../../images/ce-fn-webhook.png)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""main entry point to webhook function"""
2+
import logging
3+
import os
4+
import json
5+
import httpx
6+
from helpers import verify_payload, verify_signature, get_iam_token
7+
8+
HEADERS = {"Content-Type": "text/plain;charset=utf-8"}
9+
10+
logger = logging.getLogger()
11+
12+
13+
def main(params):
14+
ibmcloud_api_key = os.environ.get('IBMCLOUD_API_KEY')
15+
if not ibmcloud_api_key:
16+
raise ValueError("IBMCLOUD_API_KEY environment variable not found")
17+
18+
secret_token = os.environ.get("WEBHOOK_SECRET")
19+
if not secret_token:
20+
raise ValueError("WEBHOOK_SECRET environment variable not found")
21+
22+
payload_body = params
23+
headers = payload_body["__ce_headers"]
24+
signature_header = headers.get("X-Hub-Signature-256", None)
25+
image_tag = payload_body.get('workflow_run', {}).get('head_sha', None)
26+
if not image_tag:
27+
return {
28+
"headers": {"Content-Type": "application/json"},
29+
"statusCode": 400,
30+
"body": "Missing image tag"
31+
}
32+
verify_payload(payload_body)
33+
verify_signature(payload_body, secret_token, signature_header)
34+
35+
iam_token = get_iam_token(ibmcloud_api_key)
36+
if not iam_token:
37+
return {
38+
"headers": HEADERS,
39+
"statusCode": 500,
40+
"body": "Failed to get IAM token",
41+
}
42+
43+
code_engine_app = os.environ.get('CE_APP')
44+
code_engine_region = os.environ.get('CE_REGION')
45+
project_id = os.environ.get('CE_PROJECT_ID')
46+
app_endpoint = f"https://api.{code_engine_region}.codeengine.cloud.ibm.com/v2/projects/{project_id}/apps/{code_engine_app}"
47+
48+
try:
49+
50+
app_get = httpx.get(app_endpoint, headers = { "Authorization" : iam_token })
51+
results = app_get.json()
52+
etag = results['entity_tag']
53+
short_tag = image_tag[:8]
54+
update_headers = { "Authorization" : iam_token, "Content-Type" : "application/merge-patch+json", "If-Match" : etag }
55+
app_patch_model = { "image_reference": "private.us.icr.io/rtiffany/dts-ce-py-app:" + short_tag }
56+
app_update = httpx.patch(app_endpoint, headers = update_headers, json = app_patch_model)
57+
app_update.raise_for_status()
58+
app_json_payload = app_update.json()
59+
latest_ready_revision = app_json_payload.get('latest_ready_revision', None)
60+
61+
data = {
62+
"headers": {"Content-Type": "application/json"},
63+
"statusCode": 200,
64+
"latest_ready_revision": latest_ready_revision,
65+
"body": "App updated successfully"
66+
}
67+
68+
return {
69+
"headers": {"Content-Type": "application/json"},
70+
"statusCode": 200,
71+
"body": json.dumps(data)
72+
}
73+
except httpx.HTTPError as e:
74+
# Define results here to avoid the error
75+
results = {"error": str(e)}
76+
return {
77+
"headers": {"Content-Type": "application/json"},
78+
"statusCode": 500,
79+
"body": json.dumps(results)
80+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import json
2+
import hashlib
3+
import hmac
4+
import httpx
5+
6+
HEADERS = {"Content-Type": "text/plain;charset=utf-8"}
7+
8+
9+
def verify_payload(params):
10+
"""Verify X-Hub-Signature-256, commits, & head_commit.id exist."""
11+
if (
12+
"__ce_headers" not in params
13+
or "X-Hub-Signature-256" not in params["__ce_headers"]
14+
):
15+
return {
16+
"headers": HEADERS,
17+
"body": "Missing params.headers.X-Hub-Signature-256",
18+
}
19+
20+
if "workflow_run" not in params:
21+
return {
22+
"headers": HEADERS,
23+
"body": "Missing params.workflow_run",
24+
}
25+
26+
return None
27+
28+
def verify_signature(payload_body, secret_token, signature_header):
29+
"""Verify that the payload was sent from GitHub by validating SHA256.
30+
31+
Raise and return 403 if not authorized.
32+
33+
Args:
34+
payload_body: original request body to verify (request.body())
35+
secret_token: GitHub app webhook token (WEBHOOK_SECRET)
36+
signature_header: header received from GitHub (x-hub-signature-256)
37+
"""
38+
for value in payload_body:
39+
value: str
40+
if value.startswith("__"):
41+
del value
42+
payload_body_bytes = json.dumps(payload_body).encode('utf-8')
43+
44+
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body_bytes, digestmod=hashlib.sha256)
45+
expected_signature = "sha256=" + hash_object.hexdigest()
46+
47+
if not hmac.compare_digest(expected_signature, signature_header):
48+
return {
49+
"statusCode": 403,
50+
"headers": HEADERS,
51+
"body": "Request signatures didn't match!",
52+
}
53+
54+
return None
55+
56+
def get_iam_token(ibmcloud_api_key):
57+
"""Get IAM token from IBM Cloud using API key."""
58+
hdrs = { "Accept" : "application/json", "Content-Type" : "application/x-www-form-urlencoded" }
59+
iam_params = { "grant_type" : "urn:ibm:params:oauth:grant-type:apikey", "apikey" : ibmcloud_api_key }
60+
resp = httpx.post('https://iam.cloud.ibm.com/identity/token', data = iam_params, headers = hdrs)
61+
resp.raise_for_status()
62+
return resp.json().get('access_token', None)
63+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
annotated-types==0.6.0
2+
anyio==4.3.0
3+
certifi==2024.2.2
4+
click==8.1.7
5+
dnspython==2.6.1
6+
email_validator==2.1.1
7+
fastapi==0.111.0
8+
fastapi-cli==0.0.3
9+
h11==0.14.0
10+
httpcore==1.0.5
11+
httptools==0.6.1
12+
httpx==0.27.0
13+
idna==3.7
14+
Jinja2==3.1.4
15+
markdown-it-py==3.0.0
16+
MarkupSafe==2.1.5
17+
mdurl==0.1.2
18+
orjson==3.10.3
19+
pydantic==2.7.1
20+
pydantic_core==2.18.2
21+
Pygments==2.18.0
22+
python-dotenv==1.0.1
23+
python-multipart==0.0.9
24+
PyYAML==6.0.1
25+
rich==13.7.1
26+
shellingham==1.5.4
27+
sniffio==1.3.1
28+
starlette==0.37.2
29+
typer==0.12.3
30+
typing_extensions==4.11.0
31+
ujson==5.9.0
32+
uvicorn==0.29.0
33+
uvloop==0.19.0
34+
watchfiles==0.21.0
35+
websockets==12.0

images/ce-fn-webhook.png

81.8 KB
Loading

0 commit comments

Comments
 (0)