Skip to content

Commit 1e0433f

Browse files
committed
supervisor: Add testing_farm_utils.py for Testing Farm integration
We'll want to start Testing Farm tests when we hit test failures so that we can see if they happened with the previous build - add a new module to handle Testing Farm API interactions, with a specific function to cover recreating a Testing Farm request based on an existing one but changing the build NVR.
1 parent 88f273d commit 1e0433f

File tree

5 files changed

+237
-2
lines changed

5 files changed

+237
-2
lines changed

openshift/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
oc create secret generic jotnar-bot-keytab --from-file=jotnar-bot.keytab
2020
```
2121

22+
`testing-farm-env`:
23+
```
24+
TESTING_FARM_API_TOKEN
25+
```
26+
2227
Values of these secrets are documented in [README](https://github.com/packit/jotnar?tab=readme-ov-file#service-accounts--authentication).
2328

2429
- Create RHEL configuration ConfigMap manually:

supervisor/main.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ class State:
3434

3535

3636
def check_env(
37-
chat: bool = False, jira: bool = False, redis: bool = False, gitlab: bool = False
37+
chat: bool = False,
38+
jira: bool = False,
39+
redis: bool = False,
40+
gitlab: bool = False,
41+
testing_farm: bool = False,
3842
):
3943
required_vars = []
4044
if chat:
@@ -51,6 +55,8 @@ def check_env(
5155
)
5256
if gitlab:
5357
required_vars.append(("GITLAB_TOKEN", "Gitlab authentication token"))
58+
if testing_farm:
59+
required_vars.append(("TESTING_FARM_API_TOKEN", "Testing Farm API token"))
5460

5561
missing_vars = [var for var in required_vars if not os.getenv(var[0])]
5662

supervisor/supervisor_types.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime
22
from enum import StrEnum
3-
from typing import Optional
3+
from typing import Any, Optional
44

55
from pydantic import BaseModel, Field
66
from typing_extensions import Literal
@@ -113,6 +113,54 @@ class FullIssue(Issue):
113113
comments: list[JiraComment]
114114

115115

116+
class TestingFarmRequestState(StrEnum):
117+
NEW = "new"
118+
QUEUED = "queued"
119+
RUNNING = "running"
120+
ERROR = "error"
121+
CANCELED = "canceled"
122+
CANCEL_REQUESTED = "cancel-requested"
123+
COMPLETE = "complete"
124+
125+
126+
class TestingFarmRequestResult(StrEnum):
127+
PASSED = "passed"
128+
FAILED = "failed"
129+
SKIPPED = "skipped"
130+
UNKNOWN = "unknown"
131+
ERROR = "error"
132+
133+
134+
class TestingFarmRequest(BaseModel):
135+
id: str
136+
url: str
137+
state: TestingFarmRequestState
138+
result: TestingFarmRequestResult = TestingFarmRequestResult.UNKNOWN
139+
result_xunit_url: str | None = None
140+
created: datetime
141+
updated: datetime
142+
143+
# We save the raw data to use during test reproduction
144+
test_data: dict[str, Any]
145+
environments_data: list[dict[str, Any]]
146+
147+
@property
148+
def arches(self) -> list[str]:
149+
return [env["arch"] for env in self.environments_data]
150+
151+
@property
152+
def build_nvr(self) -> str:
153+
versions = {
154+
variables["BUILDS"]
155+
for env in self.environments_data
156+
if (variables := env.get("variables")) and "BUILDS" in variables
157+
}
158+
if len(versions) != 1:
159+
raise ValueError("Can't determine package version for request")
160+
161+
return versions.pop()
162+
163+
116164
class JotnarTag(BaseModel):
117165
"""
118166
A magic string appearing in the description of an issue that

supervisor/testing_farm_utils.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from datetime import datetime, timezone
2+
from functools import cache
3+
from json import dumps as json_dumps
4+
import logging
5+
import os
6+
from typing import Any
7+
8+
9+
from .http_utils import requests_session
10+
from .supervisor_types import (
11+
TestingFarmRequest,
12+
TestingFarmRequestResult,
13+
TestingFarmRequestState,
14+
)
15+
16+
17+
TESTING_FARM_URL = "https://api.testing-farm.io/v0.1"
18+
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
@cache
24+
def testing_farm_headers() -> dict[str, str]:
25+
token = os.environ["TESTING_FARM_API_TOKEN"]
26+
27+
return {
28+
"Authorization": f"Bearer {token}",
29+
"Content-Type": "application/json",
30+
}
31+
32+
33+
def testing_farm_api_get(path: str, *, params: dict | None = None) -> Any:
34+
url = f"{TESTING_FARM_URL}/{path}"
35+
response = requests_session().get(
36+
url, headers=testing_farm_headers(), params=params
37+
)
38+
if not response.ok:
39+
logger.error(
40+
"GET %s%s failed.\nerror:\n%s",
41+
url,
42+
f" (params={params})" if params else "",
43+
response.text,
44+
)
45+
response.raise_for_status()
46+
return response.json()
47+
48+
49+
def testing_farm_api_post(
50+
path: str,
51+
json: dict[str, Any],
52+
) -> Any:
53+
url = f"{TESTING_FARM_URL}/{path}"
54+
response = requests_session().post(url, headers=testing_farm_headers(), json=json)
55+
if not response.ok:
56+
logger.error(
57+
"POST to %s failed\nbody:\n%s\nerror:\n%s",
58+
url,
59+
json_dumps(json, indent=2),
60+
response.text,
61+
)
62+
response.raise_for_status()
63+
return response.json()
64+
65+
66+
fake_testing_id_counter = 0
67+
68+
69+
def testing_farm_reproduce_request_with_build(
70+
request: TestingFarmRequest,
71+
build_nvr: str,
72+
dry_run: bool = False,
73+
) -> TestingFarmRequest:
74+
"""
75+
Create a Testing Farm request to reproduce an existing request with a different build.
76+
77+
Args:
78+
request: The original Testing Farm request.
79+
build_nvr: The NVR of the build to use for reproduction.
80+
81+
Returns:
82+
The new Testing Farm request.
83+
84+
Raises:
85+
HTTPError: If the API request fails.
86+
"""
87+
original_environments = request.environments_data
88+
89+
# We manually construct the environment to replace the build
90+
# and skip newa_ variables. There are some other keys in the
91+
# environment dict that we don't copy over - in particular
92+
# "hardware" and "kickstart" - these shouldn't be relevant.
93+
environments = [
94+
{
95+
"arch": env["arch"],
96+
"os": env["os"],
97+
"variables": env["variables"] | {"BUILDS": build_nvr},
98+
"tmt": {
99+
"context": {
100+
k: v
101+
for k, v in env["tmt"]["context"].items()
102+
if not k.startswith("newa_")
103+
}
104+
},
105+
}
106+
for env in original_environments
107+
]
108+
109+
body = {
110+
"test": request.test_data,
111+
"environments": environments,
112+
}
113+
if dry_run:
114+
logger.info(
115+
"Dry run: would start Testing Farm request reproducing %s with build %s",
116+
request.id,
117+
build_nvr,
118+
)
119+
logger.debug("Dry run: would post %s to %s", body, "requests")
120+
global fake_testing_id_counter
121+
fake_testing_id_counter += 1
122+
test_id = f"fake-testing-id-{fake_testing_id_counter}"
123+
return TestingFarmRequest(
124+
id=test_id,
125+
url=f"{TESTING_FARM_URL}/requests/{test_id}",
126+
state=TestingFarmRequestState.NEW,
127+
created=datetime.now(tz=timezone.utc),
128+
updated=datetime.now(tz=timezone.utc),
129+
test_data=body["test"],
130+
environments_data=body["environments"],
131+
)
132+
133+
response = testing_farm_api_post("requests", json=body)
134+
135+
return TestingFarmRequest(
136+
id=response["id"],
137+
url=f"{TESTING_FARM_URL}/requests/{response['id']}",
138+
state=response["state"],
139+
created=datetime.fromisoformat(response["created"]),
140+
updated=datetime.fromisoformat(response["updated"]),
141+
test_data=response["test"],
142+
environments_data=response["environments"],
143+
)
144+
145+
146+
def testing_farm_get_request(request_id: str) -> TestingFarmRequest:
147+
"""
148+
Retrieve details about a Testing Farm request by its ID.
149+
150+
Args:
151+
request_id: The ID of the Testing Farm request.
152+
153+
Returns:
154+
The Testing Farm request.
155+
"""
156+
response = testing_farm_api_get(f"requests/{request_id}")
157+
158+
result_data = response.get("result")
159+
160+
return TestingFarmRequest(
161+
id=response["id"],
162+
url=f"{TESTING_FARM_URL}/requests/{response['id']}",
163+
state=response["state"],
164+
result=(
165+
result_data["overall"] if result_data else TestingFarmRequestResult.UNKNOWN
166+
),
167+
result_xunit_url=result_data.get("xunit_url") if result_data else None,
168+
created=datetime.fromisoformat(response["created"]),
169+
updated=datetime.fromisoformat(response["updated"]),
170+
test_data=response["test"],
171+
environments_data=response["environments_requested"],
172+
)

templates/supervisor.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ JIRA_URL=https://issues.redhat.com
1212
# Required: Jira API token (Bearer token)
1313
# Get this from: https://issues.redhat.com/secure/ViewProfile.jspa?selectedTab=com.atlassian.pats.pats-plugin:jira-user-personal-access-tokens
1414
JIRA_TOKEN=
15+
16+
# Required: testing farm API token (Bearer token)
17+
# Get this from https://testing-farm.io/tokens/ (select the "Red Hat" ranch)
18+
TESTING_FARM_API_TOKEN=

0 commit comments

Comments
 (0)