Skip to content

Commit 34d886f

Browse files
committed
fix: Modify pin_safe_versions script to install newer packages after grace period ended
Signed-off-by: Cagri Yonca <cagri@ibm.com>
1 parent c9639d7 commit 34d886f

1 file changed

Lines changed: 298 additions & 0 deletions

File tree

.circleci/pin_safe_versions.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# Standard Libraries
2+
import json
3+
import re
4+
from datetime import datetime
5+
6+
import pandas as pd
7+
8+
# Third Party
9+
import requests
10+
from bs4 import BeautifulSoup
11+
from kubernetes import client, config
12+
from packaging.version import Version
13+
14+
JSON_FILE = "resources/table.json"
15+
REPORT_FILE = "docs/report.md"
16+
PIP_INDEX_URL = "https://pypi.org/pypi"
17+
PEP_BASE_URL = "https://peps.python.org/"
18+
19+
SPEC_MAP = {
20+
"ASGI": "https://asgi.readthedocs.io/en/latest/specs/main.html",
21+
"WSGI": "https://peps.python.org/numerical",
22+
}
23+
24+
25+
def estimate_days_behind(release_date):
26+
return (datetime.today().date() - datetime.strptime(release_date, "%Y-%m-%d").date()).days
27+
28+
29+
def get_upstream_version(dependency, last_supported_version):
30+
"""Get the latest version available upstream"""
31+
last_supported_version_release_date = "Not found"
32+
if dependency in SPEC_MAP:
33+
# webscrape info from official website
34+
version_pattern = r"(\d+\.\d+\.?\d*)"
35+
latest_version_release_date = ""
36+
37+
url = SPEC_MAP[dependency]
38+
page = requests.get(url)
39+
soup = BeautifulSoup(page.text, "html.parser")
40+
# ASGI
41+
if "asgi" in url:
42+
all_versions = soup.find(id="version-history").find_all("li")
43+
pattern = re.compile(r"([\d.]+) \((\d{4}-\d{2}-\d{2})\)")
44+
latest_version, latest_version_release_date = pattern.search(
45+
all_versions[0].text
46+
).groups()
47+
for li in all_versions:
48+
match = pattern.search(li.text)
49+
if match:
50+
version, date = match.groups()
51+
if version == last_supported_version:
52+
last_supported_version_release_date = date
53+
break
54+
# WSGI
55+
else:
56+
all_versions = soup.find(id="numerical-index").find_all(
57+
"a", string=re.compile("Web Server Gateway Interface")
58+
)
59+
latest_version = re.search(version_pattern, all_versions[-1].text).group()
60+
61+
for a in all_versions:
62+
pep_link = PEP_BASE_URL + a.get("href").split("..")[1]
63+
response = requests.get(pep_link)
64+
soup = BeautifulSoup(response.text, "html.parser")
65+
version = re.search(version_pattern, a.text).group()
66+
pep_page_metadata = soup.find("dl")
67+
68+
if pep_page_metadata and version in [
69+
latest_version,
70+
last_supported_version,
71+
]:
72+
metadata_fields = pep_page_metadata.find_all("dt")
73+
metadata_values = pep_page_metadata.find_all("dd")
74+
75+
for dt, dd in zip(metadata_fields, metadata_values):
76+
if "Created" in dt.text:
77+
release_date = dd.text.strip()
78+
release_date_as_datetime = datetime.strptime(
79+
release_date, "%d-%b-%Y"
80+
)
81+
if version == latest_version:
82+
latest_version_release_date = (
83+
release_date_as_datetime.strftime("%Y-%m-%d")
84+
)
85+
if version == last_supported_version:
86+
last_supported_version_release_date = (
87+
release_date_as_datetime.strftime("%Y-%m-%d")
88+
)
89+
return (
90+
latest_version,
91+
latest_version_release_date,
92+
last_supported_version_release_date,
93+
)
94+
95+
else:
96+
# get info using PYPI API
97+
response = requests.get(f"{PIP_INDEX_URL}/{dependency}/json")
98+
response_json = response.json()
99+
100+
latest_version = response_json["info"]["version"]
101+
release_info_latest = response_json["releases"][latest_version]
102+
release_time_latest = release_info_latest[-1]["upload_time_iso_8601"]
103+
release_date_latest = re.search(r"([\d-]+)T", release_time_latest)[1]
104+
105+
release_info_last_supported = response_json["releases"][last_supported_version]
106+
release_time_last_supported = release_info_last_supported[-1]["upload_time_iso_8601"]
107+
release_date_last_supported = re.search(r"([\d-]+)T", release_time_last_supported)[1]
108+
109+
return (
110+
latest_version,
111+
release_date_latest,
112+
release_date_last_supported,
113+
)
114+
115+
116+
def get_last_supported_version(tekton_ci_output, dependency):
117+
"""Get up-to-date supported version"""
118+
if dependency == "Psycopg2":
119+
dependency = "psycopg2-binary"
120+
121+
# either start with a space or in a new line
122+
pattern = r"(?:^|\s)" + dependency + r"-([^\s]+)"
123+
124+
last_supported_version = re.search(
125+
pattern, tekton_ci_output, flags=re.I | re.M
126+
)
127+
128+
return last_supported_version[1]
129+
130+
131+
def is_up_to_date(
132+
last_supported_version, latest_version, latest_version_release_date
133+
):
134+
"""Check if the supported package is up-to-date"""
135+
if Version(last_supported_version) >= Version(latest_version):
136+
up_to_date = "Yes"
137+
days_behind = 0
138+
else:
139+
up_to_date = "No"
140+
days_behind = estimate_days_behind(latest_version_release_date)
141+
142+
return up_to_date, days_behind
143+
144+
def taskrun_filter(taskrun):
145+
return any(
146+
condition["type"] == "Succeeded" and condition["status"] == "True"
147+
for condition in taskrun["status"]["conditions"]
148+
)
149+
150+
def get_taskruns(namespace, task_name):
151+
"""Get sorted taskruns filtered based on label_selector"""
152+
group = "tekton.dev"
153+
version = "v1"
154+
plural = "taskruns"
155+
156+
# access the custom resource from tekton
157+
tektonV1 = client.CustomObjectsApi()
158+
taskruns = tektonV1.list_namespaced_custom_object(
159+
group,
160+
version,
161+
namespace,
162+
plural,
163+
label_selector=f"{group}/task={task_name}, triggers.tekton.dev/trigger=python-tracer-scheduled-pipeline-triggger",
164+
)["items"]
165+
166+
filtered_taskruns = list(filter(taskrun_filter, taskruns))
167+
filtered_taskruns.sort(
168+
key=lambda tr: tr["metadata"]["creationTimestamp"], reverse=True
169+
)
170+
171+
return filtered_taskruns
172+
173+
174+
def process_taskrun_logs(
175+
taskruns, core_v1_client, namespace, task_name, tekton_ci_output
176+
):
177+
"""Process taskrun logs"""
178+
for tr in taskruns:
179+
pod_name = tr["status"]["podName"]
180+
taskrun_name = tr["metadata"]["name"]
181+
logs = core_v1_client.read_namespaced_pod_log(
182+
pod_name, namespace, container="step-unittest"
183+
)
184+
if "Successfully installed" in logs:
185+
print(
186+
f"Retrieving container logs from the successful taskrun pod {pod_name} of taskrun {taskrun_name}.."
187+
)
188+
if task_name == "python-tracer-unittest-gevent-starlette-task":
189+
match = re.search(r"Successfully installed .*(gevent-[^\s]+) .* (starlette-[^\s]+)", logs)
190+
tekton_ci_output += f"{match[1]}\n{match[2]}\n"
191+
elif task_name == "python-tracer-unittest-kafka-task":
192+
match = re.search(r"Successfully installed .*(confluent-kafka-[^\s]+) .* (kafka-python-ng-[^\s]+)", logs)
193+
tekton_ci_output += f"{match[1]}\n{match[2]}\n"
194+
elif task_name == "python-tracer-unittest-cassandra-task":
195+
match = re.search(r"Successfully installed .*(cassandra-driver-[^\s]+)", logs)
196+
tekton_ci_output += f"{match[1]}\n"
197+
elif task_name == "python-tracer-unittest-default-task":
198+
lines = re.findall(r"^Successfully installed .*", logs, re.M)
199+
tekton_ci_output += "\n".join(lines)
200+
break
201+
else:
202+
print(
203+
f"Unable to retrieve container logs from the successful taskrun pod {pod_name} of taskrun {taskrun_name}."
204+
)
205+
return tekton_ci_output
206+
207+
208+
def get_tekton_ci_output():
209+
"""Get the latest successful scheduled tekton pipeline output"""
210+
try:
211+
config.load_incluster_config()
212+
print("Using in-cluster Kubernetes configuration...")
213+
except config.config_exception.ConfigException:
214+
# Fall back to local config if running locally and not inside cluster
215+
config.load_kube_config()
216+
print("Using local Kubernetes configuration...")
217+
218+
namespace = "default"
219+
core_v1_client = client.CoreV1Api()
220+
221+
tasks = [
222+
"python-tracer-unittest-gevent-starlette-task",
223+
"python-tracer-unittest-kafka-task",
224+
"python-tracer-unittest-cassandra-task",
225+
"python-tracer-unittest-default-task"
226+
]
227+
228+
tekton_ci_output = ""
229+
230+
for task_name in tasks:
231+
try:
232+
taskruns = get_taskruns(namespace, task_name)
233+
234+
tekton_ci_output = process_taskrun_logs(
235+
taskruns, core_v1_client, namespace, task_name, tekton_ci_output
236+
)
237+
except Exception as exc:
238+
print(f"Error processing task {task_name}: {str(exc)}")
239+
240+
return tekton_ci_output
241+
242+
243+
def main():
244+
# Read the JSON file
245+
with open(JSON_FILE) as file:
246+
data = json.load(file)
247+
248+
items = data["table"]
249+
tekton_ci_output = get_tekton_ci_output()
250+
251+
for item in items:
252+
package = item["Package name"]
253+
254+
if "Last Supported Version" not in item:
255+
last_supported_version = get_last_supported_version(
256+
tekton_ci_output, package
257+
)
258+
item.update({"Last Supported Version": last_supported_version})
259+
else:
260+
last_supported_version = item["Last Supported Version"]
261+
262+
latest_version, latest_version_release_date, last_supported_version_release_date = (
263+
get_upstream_version(package, last_supported_version)
264+
)
265+
266+
up_to_date, days_behind = is_up_to_date(
267+
last_supported_version, latest_version, latest_version_release_date
268+
)
269+
270+
item.update(
271+
{
272+
"Latest version": latest_version,
273+
"Up-to-date": up_to_date,
274+
"Release date": last_supported_version_release_date,
275+
"Latest Version Published At": latest_version_release_date,
276+
"Days behind": f"{days_behind} day/s",
277+
}
278+
)
279+
280+
# Create a DataFrame from the list of dictionaries
281+
df = pd.DataFrame(items)
282+
df.insert(len(df.columns) - 1, "Cloud Native", df.pop("Cloud Native"))
283+
284+
# Convert dataframe to markdown
285+
markdown_table = df.to_markdown(index=False)
286+
287+
disclaimer = "##### This page is auto-generated. Any change will be overwritten after the next sync. Please apply changes directly to the files in the [python tracer](https://github.com/instana/python-sensor) repo."
288+
title = "## Python supported packages and versions"
289+
290+
# Combine disclaimer, title, and markdown table with line breaks
291+
final_markdown = f"{disclaimer}\n{title}\n{markdown_table}\n"
292+
293+
with open(REPORT_FILE, "w") as file:
294+
file.write(final_markdown)
295+
296+
297+
if __name__ == "__main__":
298+
main()

0 commit comments

Comments
 (0)