Skip to content

Commit 012bd17

Browse files
[CI] Add dispatch_job script
This patch adds the dispatch_job python script. This script is designed to be invoked from within a llvm-zorg AnnotatedBuilder and spawn jobs inside of kubernetes pods on the premerge cluster. This is not directly integrated into an AnnotatedBuilder given the additional dependencies that we have on the kubernetes client API. Reviewers: lnihlen, dschuff, Keenuts, gburgessiv, cmtice Reviewed By: cmtice Pull Request: #526
1 parent 772b264 commit 012bd17

File tree

4 files changed

+320
-0
lines changed

4 files changed

+320
-0
lines changed

premerge/buildbot/dispatch_job.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""Dispatches a job to the k8s cluster.
2+
3+
This script takes in a commit SHA to test along with the platform, spawns a job
4+
to test it, and then streams the logs from the job. We read logs from the job
5+
every so often using the kuberntes logging API rather than directly executing
6+
commands inside the container and streaming the output. This is to work
7+
around https://github.com/kubernetes-sigs/apiserver-network-proxy/issues/748.
8+
"""
9+
10+
import sys
11+
import logging
12+
import time
13+
import dateutil
14+
import datetime
15+
import json
16+
17+
import kubernetes
18+
19+
PLATFORM_TO_NAMESPACE = {"Linux": "llvm-premerge-linux-buildbot"}
20+
LOG_SECONDS_TO_QUERY = 10
21+
SECONDS_QUERY_LOGS_EVERY = 5
22+
23+
24+
def start_build_linux(commit_sha: str, k8s_client) -> str:
25+
"""Spawns a pod to build/test LLVM at the specified SHA.
26+
27+
Args:
28+
commit_sha: The commit SHA to build/run the tests at.
29+
k8s_client: The kubernetes client instance to use for spawning the pod.
30+
31+
Returns:
32+
A string containing the name of the pod.
33+
"""
34+
pod_name = f"build-{commit_sha}"
35+
commands = [
36+
"git clone --depth 100 https://github.com/llvm/llvm-project",
37+
"cd llvm-project",
38+
f"git checkout ${commit_sha}",
39+
"export CC=clang",
40+
"export CXX=clang++",
41+
'./.ci/monolithic-linux.sh "bolt;clang;clang-tools-extra;flang;libclc;lld;lldb;llvm;mlir;polly" "check-bolt check-clang check-clang-cir check-clang-tools check-flang check-lld check-lldb check-llvm check-mlir check-polly" "compiler-rt;libc;libcxx;libcxxabi;libunwind" "check-compiler-rt check-libc" "check-cxx check-cxxabi check-unwind" "OFF"'
42+
"echo BUILD FINISHED",
43+
]
44+
pod_definition = {
45+
"apiVersion": "v1",
46+
"kind": "Pod",
47+
"metadata": {
48+
"name": pod_name,
49+
"namespace": PLATFORM_TO_NAMESPACE["Linux"],
50+
},
51+
"spec": {
52+
"containers": [
53+
{
54+
"name": "build",
55+
"image": "ghcr.io/llvm/ci-ubuntu-24.04",
56+
"command": ["/bin/bash", "-c", ";".join(commands)],
57+
}
58+
],
59+
"restartPolicy": "Never",
60+
},
61+
}
62+
kubernetes.utils.create_from_dict(k8s_client, pod_definition)
63+
return pod_name
64+
65+
66+
def read_logs(pod_name: str, namespace: str, v1_api) -> list[str]:
67+
"""Reads logs from the specified pod.
68+
69+
Reads logs using the k8s API and returns a nicely formatted list of
70+
strings.
71+
72+
Args:
73+
pod_name: The name of the pod to read logs from.
74+
namespace: The namespace the pod is in.
75+
v1_api: The kubernetes API instance to use for querying logs.
76+
77+
Returns:
78+
A list of strings representing the log lines.
79+
"""
80+
logs = v1_api.read_namespaced_pod_log(
81+
name=pod_name,
82+
namespace=namespace,
83+
timestamps=True,
84+
since_seconds=LOG_SECONDS_TO_QUERY,
85+
)
86+
return logs.split("\n")[:-1]
87+
88+
89+
def get_logs_to_print(
90+
logs: list[str], latest_time: datetime.datetime
91+
) -> tuple[datetime.datetime, list[str]]:
92+
"""Get the logs that we should be printing.
93+
94+
This function takes in a raw list of logs along with the timestamp of the
95+
last log line to be printed and returns the new log lines that should be
96+
printed.
97+
98+
Args:
99+
logs: The raw list of log lines.
100+
latest_time: The timestamp from the last log line that was printed.
101+
102+
Returns:
103+
A tuple containing the timestamp of the last log line returned and a list
104+
of strings containing the log lines that should be printed.
105+
"""
106+
first_new_index = 0
107+
time_stamp = latest_time
108+
for log_line in logs:
109+
time_stamp_str = log_line.split(" ")[0]
110+
time_stamp = dateutil.parser.parse(time_stamp_str[:-1])
111+
if time_stamp > latest_time:
112+
break
113+
first_new_index += 1
114+
last_time_stamp = latest_time
115+
if logs:
116+
last_time_stamp_str = logs[-1].split(" ")[0]
117+
last_time_stamp = dateutil.parser.parse(last_time_stamp_str[:-1])
118+
return (last_time_stamp, logs[first_new_index:])
119+
120+
121+
def print_logs(
122+
pod_name: str, namespace: str, v1_api, lastest_time: datetime.datetime
123+
) -> tuple[bool, datetime.datetime]:
124+
"""Queries the pod and prints the relevant log lines.
125+
126+
Args:
127+
pod_name: The pod to print the logs for.
128+
namespace: The namespace the log is in.
129+
v1_api: The kubernetes API client instance to use for querying the logs.
130+
latest_time: The timestamp of the last log line to be printed.
131+
132+
Returns:
133+
A tuple containing a boolean representing whether or not the pod has
134+
finished executing and the timestamp of the last log line printed.
135+
"""
136+
logs = read_logs(pod_name, namespace, v1_api)
137+
new_time_stamp, logs_to_print = get_logs_to_print(logs, lastest_time)
138+
pod_finished = False
139+
for log_line in logs_to_print:
140+
print(log_line.split("\r")[-1])
141+
if "BUILD FINISHED" in log_line:
142+
pod_finished = True
143+
144+
return (pod_finished, new_time_stamp)
145+
146+
147+
def main(commit_sha: str, platform: str):
148+
kubernetes.config.load_kube_config()
149+
k8s_client = kubernetes.client.ApiClient()
150+
if platform == "Linux":
151+
pod_name = start_build_linux(commit_sha, k8s_client)
152+
else:
153+
raise ValueError("Unrecognized platform.")
154+
namespace = PLATFORM_TO_NAMESPACE[platform]
155+
latest_time = datetime.datetime.min
156+
v1_api = kubernetes.client.CoreV1Api()
157+
while True:
158+
try:
159+
pod_finished, latest_time = print_logs(
160+
pod_name, namespace, v1_api, latest_time
161+
)
162+
if pod_finished:
163+
break
164+
except kubernetes.client.exceptions.ApiException as log_exception:
165+
if "ContainerCreating" in json.loads(log_exception.body)["message"]:
166+
logging.warning(
167+
"Cannot yet read logs from the pod: waiting for the container to start."
168+
)
169+
else:
170+
logging.warning(f"Failed to get logs from the pod: {log_exception}")
171+
time.sleep(SECONDS_QUERY_LOGS_EVERY)
172+
v1_api.delete_namespaced_pod(pod_name, namespace)
173+
174+
175+
if __name__ == "__main__":
176+
if len(sys.argv) != 3:
177+
logging.fatal("Expected usage is dispatch_job.py {commit SHA} {platform}")
178+
sys.exit(1)
179+
main(sys.argv[1], sys.argv[2])
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Tests for the dispatch_job.py script."""
2+
3+
import unittest
4+
import datetime
5+
import dateutil
6+
7+
import dispatch_job
8+
9+
10+
class TestDispatchJobs(unittest.TestCase):
11+
def test_get_logs_first_time(self):
12+
"""Test we return the correct logs if we have not seen any before."""
13+
log_lines = [
14+
"2025-07-29T15:48:00.259595535Z test1",
15+
"2025-07-29T15:48:00.383251277Z test2",
16+
]
17+
current_timestamp = datetime.datetime.min
18+
latest_timestamp, lines_to_print = dispatch_job.get_logs_to_print(
19+
log_lines, current_timestamp
20+
)
21+
self.assertSequenceEqual(
22+
lines_to_print,
23+
[
24+
"2025-07-29T15:48:00.259595535Z test1",
25+
"2025-07-29T15:48:00.383251277Z test2",
26+
],
27+
)
28+
self.assertEqual(
29+
latest_timestamp, dateutil.parser.parse("2025-07-29T15:48:00.383251277")
30+
)
31+
32+
def test_get_logs_nonoverlapping(self):
33+
"""Test we return the correct logs for non-overlapping ranges.
34+
35+
Test that if the timestamp of the last log that we have printed is
36+
less than the current set returned by kubernetes, we return the correct
37+
lines.
38+
"""
39+
log_lines = [
40+
"2025-07-29T15:48:01.787177054Z test1",
41+
"2025-07-29T15:48:03.074715108Z test2",
42+
]
43+
current_timestamp = dateutil.parser.parse("2025-07-29T15:48:00.383251277")
44+
latest_timestamp, lines_to_print = dispatch_job.get_logs_to_print(
45+
log_lines, current_timestamp
46+
)
47+
self.assertSequenceEqual(
48+
lines_to_print,
49+
[
50+
"2025-07-29T15:48:01.787177054Z test1",
51+
"2025-07-29T15:48:03.074715108Z test2",
52+
],
53+
)
54+
self.assertEqual(
55+
latest_timestamp, dateutil.parser.parse("2025-07-29T15:48:03.074715108")
56+
)
57+
58+
def test_get_logs_overlapping(self):
59+
"""Test we return the correct logs for overlapping ranges.
60+
61+
Test that if the last line to be printed is contained within the logs
62+
kubernetes returned, we skip the lines that have already been printed.
63+
"""
64+
log_lines = [
65+
"2025-07-29T15:48:00.383251277Z test1",
66+
"2025-07-29T15:48:01.787177054Z test2",
67+
"2025-07-29T15:48:03.074715108Z test3",
68+
]
69+
current_timestamp = dateutil.parser.parse("2025-07-29T15:48:00.383251277")
70+
latest_timestamp, lines_to_print = dispatch_job.get_logs_to_print(
71+
log_lines, current_timestamp
72+
)
73+
self.assertSequenceEqual(
74+
lines_to_print,
75+
[
76+
"2025-07-29T15:48:01.787177054Z test2",
77+
"2025-07-29T15:48:03.074715108Z test3",
78+
],
79+
)
80+
self.assertEqual(
81+
latest_timestamp, dateutil.parser.parse("2025-07-29T15:48:03.074715108")
82+
)
83+
84+
85+
if __name__ == "__main__":
86+
unittest.main()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.12
3+
# by the following command:
4+
#
5+
# pip-compile --output-file=requirements.lock.txt requirements.txt
6+
#
7+
cachetools==5.5.2
8+
# via google-auth
9+
certifi==2025.7.14
10+
# via
11+
# kubernetes
12+
# requests
13+
charset-normalizer==3.4.2
14+
# via requests
15+
durationpy==0.10
16+
# via kubernetes
17+
google-auth==2.40.3
18+
# via kubernetes
19+
idna==3.10
20+
# via requests
21+
kubernetes==33.1.0
22+
# via -r requirements.txt
23+
oauthlib==3.3.1
24+
# via
25+
# kubernetes
26+
# requests-oauthlib
27+
pyasn1==0.6.1
28+
# via
29+
# pyasn1-modules
30+
# rsa
31+
pyasn1-modules==0.4.2
32+
# via google-auth
33+
python-dateutil==2.9.0.post0
34+
# via kubernetes
35+
pyyaml==6.0.2
36+
# via kubernetes
37+
requests==2.32.4
38+
# via
39+
# kubernetes
40+
# requests-oauthlib
41+
requests-oauthlib==2.0.0
42+
# via kubernetes
43+
rsa==4.9.1
44+
# via google-auth
45+
six==1.17.0
46+
# via
47+
# kubernetes
48+
# python-dateutil
49+
urllib3==2.5.0
50+
# via
51+
# kubernetes
52+
# requests
53+
websocket-client==1.8.0
54+
# via kubernetes

premerge/buildbot/requirements.txt

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

0 commit comments

Comments
 (0)