ถ้าใครที่กำลังใช้ Google Cloud Build เพื่อทำ CI (Continuous Integration) และมี Google Workspace หรือชื่อเก่าคือ G-Suite เราสามารถสร้าง process ให้มีข้อความบอกทันทีที่ Build เสร็จได้แล้วนะฮะ ตามนี้เลย
ต้องบอกกันก่อนว่าควรจะมีพื้นฐานภาษา Python ด้วยนะฮะ
Overview
1. Google Cloud Build
จุดเริ่มต้นของจักรวาลนี้ คือ เราต้องมี Google Cloud Build trigger กันก่อนนะฮะ ก็สร้างตามลิงก์นี้ ได้เลย
สมมติว่าเรามี source code repo และ Cloud Build trigger ที่ทำหน้าที่ CI เรียบร้อยแล้ว ก็ไปข้อต่อไปเลยฮะ
2. Google Cloud Pub/Sub
เมื่อเรามี Cloud Build trigger แล้วจะต้องมี Cloud Pub/Sub topic ชื่อ “cloud-builds” ฮะ ให้เราสร้าง topic นี้ถ้ายังไม่มีนะ และถ้าเราสร้าง subscriber มาลอง pull ดู จะได้ message ตาม format นี้ฮะ
{
"name": "projects/123/locations/global/builds/ca523374-cf65-4acc-b13f-0c32b7c5f0b0",
"id": "ca523374-cf65-4acc-b13f-0c32b7c5f0b0",
"projectId": "project",
"status": "SUCCESS",
"source": {
...
},
"steps": [
{
...
}
],
"results": {
...
},
"createTime": "2021-02-19T06:27:49.268719440Z",
"startTime": "2021-02-19T06:27:50.900538766Z",
"finishTime": "2021-02-19T06:29:32.266544Z",
"timeout": "1200s",
"queueTtl": "3600s",
"logsBucket": "gs://log-bucket",
"sourceProvenance": {
...
},
"buildTriggerId": "e6c46525-39be-4886-afe6-d98c18d6893e",
"options": {
...
},
"logUrl": "https://console.cloud.google.com/cloud-build/builds/ca523374-cf65-4acc-b13f-0c32b7c5f0b0?project=123",
"substitutions": {
"BRANCH_NAME": "branch",
"COMMIT_SHA": "b7738ee8b38e1a8551ec632e8e79733c9a546fa1",
"REPO_NAME": "repo",
"REVISION_ID": "b7738ee8b38e1a8551ec632e8e79733c9a546fa1",
"SHORT_SHA": "b7738ee"
},
"tags": ["trigger-e6c46525-39be-4886-afe6-d98c18d6893e"],
"timing": {
"BUILD": {
"startTime": "2021-02-19T06:28:03.203464932Z",
"endTime": "2021-02-19T06:29:27.751418550Z"
},
"FETCHSOURCE": {
"startTime": "2021-02-19T06:27:53.686032686Z",
"endTime": "2021-02-19T06:28:03.203377780Z"
}
}
}
ตัวอย่าง payload ที่จะได้จาก Google Cloud Build
3. Google Chat
ต่อมาก็ต้องสร้างปลายทาง นั่นคือห้องใน Google Chat และสร้าง incoming webhook ตามลิงก์นี้ ฮะ
4. Google Cloud Functions
ส่วนสุดท้ายคือสร้าง Google Cloud Functions มาเชื่อมกับ Pub/Sub ที่ว่า และกำหนดให้มันทำงานเมื่อมีข้อความถูก push เข้ามา ก่อนจะส่งไปยัง Google Chat ตาม webhook ที่เพิ่งสร้างไปตะกี้นั่นเองฮะ
ถ้าเสร็จแล้ว เรามาเริ่มเขียน Google Cloud Functions กัน
4.1 ตั้งต้นด้วย function รับข้อความจาก Pub/Sub
สำหรับ Google Cloud Functions ที่รองรับการ push message มาจาก pub/sub ใช้รูปแบบตามนี้ฮะ
import base64
import json
def cloudbuild_notifications(event, context):
message = json.loads(base64.b64decode(event['data']).decode('utf-8'))
แกะ message
จาก event
แค่เนี้ยแหละ เมื่อข้อความเข้ามา จะอยู่ในตัวแปร event
ใน path ชื่อ “data” ในรูปของ byte array ที่เราต้อง decode และแปลงกลับเป็น json format ฮะ เราก็จะได้ payload ในชื่อตัวแปร message
4.2 สร้าง function ส่ง Google Chat message
มี doc อธิบายการส่งข้อความไป Google Chat อยู่สองแบบ ได้แก่ text และ card แน่นอนเราเลือก card ด้วยความหรูหราของข้อความฮะ
from httplib2 import Http
from dateutil.parser import isoparse
from datetime import datetime, timezone
import pytz
def send_ggchat(payload):
time_pattern = "%Y-%m-%d %H:%M:%S"
time_timezone = pytz.timezone("Asia/Bangkok")
time_start = isoparse(payload['timing']['FETCHSOURCE']['startTime']).astimezone(time_timezone)
time_end = isoparse(payload['timing']['BUILD']['endTime']).astimezone(time_timezone)
message = """{{ "cards": [{{
"header": {{
"title": "Build Notification for Backend Core",
"subtitle": "Build {build_id} is {status}"
}},
"sections": [{{
"widgets": [
{{"keyValue": {{"topLabel": "Repo", "content": "{repo}"}} }},
{{"keyValue": {{"topLabel": "Branch", "content": "{branch}"}} }},
{{"keyValue": {{"topLabel": "Commit", "content": "{commit}"}} }},
{{"keyValue": {{"topLabel": "Created", "content": "{created_time}"}} }},
{{"keyValue": {{"topLabel": "Status", "content": "{status}"}} }},
{{"keyValue": {{"topLabel": "Duration (sec)", "content": "{duration}"}} }},
{{"buttons": [{{
"textButton": {{
"text": "{build_id} log Link",
"onClick": {{"openLink": {{"url": "{log_url}"}} }}
}}
}} ] }}
]
}} ]
}} ] }} """.format(
build_id=payload['id'].split("-")[0],
status=payload['status'],
repo=payload['substitutions']['REPO_NAME'],
branch=payload['substitutions']['BRANCH_NAME'],
commit=payload['substitutions']['SHORT_SHA'],
created_time=time_start.strftime(time_pattern),
duration=(time_end - time_start).total_seconds(),
log_url=payload['logUrl'],
)
message_headers = {'Content-Type': 'application/json; charset=UTF-8'}
http_obj = Http()
response = http_obj.request(
uri="https://chat.googleapis.com/v1/spaces/abc/messages?key=key&token=token",
method='POST',
headers=message_headers,
body=json.dumps(json.loads(message))
)
สร้าง message card แล้วค่อยส่งไป Google Chat ตรง message จะเป็น json string ของ card template ฮะ โดยออกแบบประมาณนี้
แสดง build id และ branch short id ด้วย ข้อมูลวันที่อยู่ในรูปแบบ nanosecond เช่น "2021-02-19T06:28:04.329309285Z"
จึงใช้ method .isoparse()
ของ library dateutil
ช่วยแปลงให้เป็น datetime
แปลง timezone จาก UTC เป็น “Asia/Bangkok” ด้วย library pytz
เพิ่มลิงก์ไปหา log ด้วย คำนวณเวลาที่ใช้ build ตั้งแต่เริ่ม "FETCHSOURCE"
จนจบ "BUILD"
ตอน call webhook ก็ใช้ method Http().request()
โดยส่ง body เป็น json string ที่ encode ถูกต้องด้วยการใช้ json.dumps(json.loads())
ผลลัพท์จะเป็นดังรูปนี้ฮะ
4.3 เปลี่ยน URL ให้เป็น environment variable
จุดนี้เพื่อเลี่ยง hardcode ในไฟล์ฮะ ตรงนี้ผมเลือกให้ environment variable ชื่อ "_URL"
และอ้างอิงตัวแปรใน code เป็น "GGCHAT_URL"
เพื่ออ้างอิงตอน call webhook
import os
GGCHAT_URL = os.environ.get('_URL', 'Environment variable does not exist')
def send_ggchat(payload):
# code ...
response = http_obj.request(
uri=GGCHAT_URL,
method='POST',
headers=message_headers,
body=json.dumps(json.loads(message))
)
4.4 รวมทุกอย่างให้เป็น main.py
call function send_ggchat()
เข้าไปใน function cloudbuild_notifications()
บันทึกเป็น file ชื่อ main.py
import base64
import json
from httplib2 import Http
from dateutil.parser import isoparse
from datetime import datetime, timezone
import pytz
import os
GGCHAT_URL = os.environ.get('_URL', 'Environment variable does not exist')
def send_ggchat(payload):
time_pattern = "%Y-%m-%d %H:%M:%S"
time_timezone = pytz.timezone("Asia/Bangkok")
time_start = isoparse(payload['timing']['FETCHSOURCE']['startTime']).astimezone(time_timezone)
time_end = isoparse(payload['timing']['BUILD']['endTime']).astimezone(time_timezone)
message = """{{ "cards": [{{
"header": {{
"title": "Build Notification for Backend Core",
"subtitle": "Build {build_id} is {status}"
}},
"sections": [{{
"widgets": [
{{"keyValue": {{"topLabel": "Repo", "content": "{repo}"}} }},
{{"keyValue": {{"topLabel": "Branch", "content": "{branch}"}} }},
{{"keyValue": {{"topLabel": "Commit", "content": "{commit}"}} }},
{{"keyValue": {{"topLabel": "Created", "content": "{created_time}"}} }},
{{"keyValue": {{"topLabel": "Status", "content": "{status}"}} }},
{{"keyValue": {{"topLabel": "Duration (sec)", "content": "{duration}"}} }},
{{"buttons": [{{
"textButton": {{
"text": "{build_id} log Link",
"onClick": {{"openLink": {{"url": "{log_url}"}} }}
}}
}} ] }}
]
}} ]
}} ] }} """.format(
build_id=payload['id'].split("-")[0],
status=payload['status'],
repo=payload['substitutions']['REPO_NAME'],
branch=payload['substitutions']['BRANCH_NAME'],
commit=payload['substitutions']['SHORT_SHA'],
created_time=time_start.strftime(time_pattern),
duration=(time_end - time_start).total_seconds(),
log_url=payload['logUrl'],
)
message_headers = {'Content-Type': 'application/json; charset=UTF-8'}
http_obj = Http()
response = http_obj.request(
uri=GGCHAT_URL,
method='POST',
headers=message_headers,
body=json.dumps(json.loads(message))
)
print(response)
def cloudbuild_notifications(event, context):
message = json.loads(base64.b64decode(event['data']).decode('utf-8'))
send_ggchat(message)
main.py
4.5 เตรียม requirements.txt
ต้องเตรียมไฟล์ requirements.txt ด้วยฮะ เพราะเราได้ import library หลายตัวเสริมเข้ามา ประมาณนี้
python-dateutil
httplib2
pytz
requirements.txt
4.6 เตรียม an environment variable file
เพราะเราได้อ้างอิง environment variable เอาไว้ ก็จำเป็นต้องเตรียมไฟล์ตรงนี้ไว้ด้วย เช่นตัวอย่างนี้ฮะ
_URL: https://chat.googleapis.com/v1/spaces/abc/messages?key=key&token=token
env.yaml
กำหนดชื่อไฟล์ว่า “env.yaml” ละกัน
4.7 จัดวางไฟล์เพื่อ deploy
ประมาณนี้ฮะ
.
├── env.yaml
└── source
├── main.py
└── requirements.txt
1 directory, 3 files
4.8 deploy function
ให้เปิด shell แล้ว prompt ที่ folder “source” แล้วพิมพ์คำสั่งนี้ (ต้องติดตั้ง Google Cloud SDK ในเครื่องแล้วนะฮะ ถ้ายัง ก็ทำตามลิงก์นี้ ได้เลย)
gcloud functions \
--project [project_id] deploy cloudbuild-notifications \
--entry-point cloudbuild_notifications \
--runtime python38 \
--retry \
--env-vars-file ../env.yaml \
--trigger-topic cloud-builds
ตรง project_id เนี่ย เราจะต้องระบุเป็นชื่อเดียวกันกับ Pub/Sub นะฮะ ไม่งั้นมันจะ trigger functions ไม่ได้นะ
4.9 เช็คผลลัพท์
ถ้าสำเร็จ เราจะได้ข้อความประมาณนี้ฮะ
Deploying function (may take a while - up to 2 minutes)...⠹
For Cloud Build Stackdriver Logs, visit: https://console.cloud.google.com/logs/viewer?project=project&advancedFilter=resource.type%3Dbuild%0Aresource.labels.build_id%3Dd13aeb6e-d08d-417e-9e2d-5bdfc55aa9e5%0AlogName%3Dprojects%2Fproject%2Flogs%2Fcloudbuild
Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
buildId: d13aeb6e-d08d-417e-9e2d-5bdfc55aa9e5
entryPoint: cloudbuild_notifications
environmentVariables:
_URL: https://chat.googleapis.com/v1/spaces/abc/messages?key=key&token=token
eventTrigger:
eventType: google.pubsub.topic.publish
failurePolicy:
retry: {}
resource: projects/project/topics/cloud-builds
service: pubsub.googleapis.com
ingressSettings: ALLOW_ALL
labels:
deployment-tool: cli-gcloud
name: projects/project/locations/us-central1/functions/cloudbuild-notifications
runtime: python38
serviceAccountEmail: [email protected]
sourceUploadUrl: https://storage.googleapis.com/xxx
status: ACTIVE
timeout: 60s
updateTime: '2021-02-19T10:56:09.074Z'
versionId: '1'
ตัวอย่าง output หลังจาก deployed เราสามารถแก้ไข format อะไรต่างๆ ตามที่เราต้องการได้เลยนะฮะ
สำหรับ source code ตัวเต็ม ดูได้ที่
GitHub - bluebirz/cloudbuild-to-googlechat
Contribute to bluebirz/cloudbuild-to-googlechat development by creating an account on GitHub.