Skip to content

Commit ba510c5

Browse files
Merge pull request #24 from majamassarini/validate-fedora-ci
Add validation for Fedora CI
2 parents fc79ee8 + b7ba6e5 commit ba510c5

12 files changed

Lines changed: 1613 additions & 167 deletions

File tree

Containerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM fedora:latest
22

3-
RUN dnf install -y python3-ogr python3-copr python3-pip && dnf clean all
3+
RUN dnf install -y python3-ogr python3-copr python3-koji python3-pip fedpkg krb5-workstation && dnf clean all
44

55
RUN pip3 install --upgrade sentry-sdk && pip3 check
66

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ classifiers = [
2626
dependencies = [
2727
"click",
2828
"copr",
29+
"koji",
2930
"ogr",
3031
"sentry-sdk",
3132
]

src/validation/cli/__init__.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,25 @@
1010

1111
from validation.tests.github import GithubTests
1212
from validation.tests.gitlab import GitlabTests
13+
from validation.tests.pagure import PagureTests
1314

14-
logging.basicConfig(level=logging.INFO)
15+
logging.basicConfig(
16+
level=logging.DEBUG,
17+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
18+
)
1519

1620

1721
@click.group(context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True)
1822
@click.version_option(prog_name="validation")
1923
def validation():
20-
loop = asyncio.get_event_loop()
21-
tasks = set()
24+
loop = asyncio.new_event_loop()
25+
asyncio.set_event_loop(loop)
26+
tasks = []
2227

2328
# GitHub
2429
if getenv("GITHUB_TOKEN"):
2530
logging.info("Running validation for GitHub.")
26-
task = loop.create_task(GithubTests().run())
27-
28-
tasks.add(task)
29-
task.add_done_callback(tasks.discard)
31+
tasks.append(GithubTests().run())
3032
else:
3133
logging.info("GITHUB_TOKEN not set, skipping the validation for GitHub.")
3234

@@ -51,15 +53,52 @@ def validation():
5153
continue
5254

5355
logging.info("Running validation for GitLab instance: %s", instance_url)
54-
task = loop.create_task(
56+
tasks.append(
5557
GitlabTests(
5658
instance_url=instance_url,
5759
namespace=namespace,
5860
token_name=token,
5961
).run(),
6062
)
6163

62-
tasks.add(task)
63-
task.add_done_callback(tasks.discard)
64+
# Pagure
65+
pagure_instances = [
66+
("https://src.fedoraproject.org/", "rpms", "PAGURE_TOKEN"),
67+
]
68+
for instance_url, namespace, token in pagure_instances:
69+
if not getenv(token):
70+
logging.info(
71+
"%s not set, skipping the validation for Pagure instance: %s",
72+
token,
73+
instance_url,
74+
)
75+
continue
76+
77+
logging.info("Running validation for Pagure instance: %s", instance_url)
78+
tasks.append(
79+
PagureTests(
80+
instance_url=instance_url,
81+
namespace=namespace,
82+
token_name=token,
83+
).run(),
84+
)
85+
86+
if not tasks:
87+
logging.error("No tokens configured, no validation tests to run")
88+
raise SystemExit(1)
89+
90+
logging.info("Running %d validation test suite(s)", len(tasks))
91+
try:
92+
results = loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
93+
logging.info("All validation tests completed")
6494

65-
loop.run_forever()
95+
# Check if any test suite failed
96+
failed_count = sum(1 for result in results if isinstance(result, Exception))
97+
if failed_count:
98+
logging.error("%d test suite(s) failed", failed_count)
99+
raise SystemExit(1)
100+
except KeyboardInterrupt:
101+
logging.info("Validation interrupted by user")
102+
raise SystemExit(130) from None
103+
finally:
104+
loop.close()

src/validation/helpers.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,35 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5+
import asyncio
56
import logging
7+
import re
8+
import subprocess
69
from functools import lru_cache
710
from os import getenv
811

12+
import koji as koji_module
913
from copr.v3 import Client
1014

1115

16+
class KerberosError(Exception):
17+
"""Exception raised for Kerberos-related errors."""
18+
19+
1220
@lru_cache
1321
def copr():
1422
return Client({"copr_url": "https://copr.fedorainfracloud.org"})
1523

1624

25+
@lru_cache
26+
def koji():
27+
"""
28+
Create and return a Koji session for querying Fedora Koji builds.
29+
"""
30+
koji_url = getenv("KOJI_URL", "https://koji.fedoraproject.org/kojihub")
31+
return koji_module.ClientSession(koji_url)
32+
33+
1734
@lru_cache
1835
def sentry_sdk():
1936
if sentry_secret := getenv("SENTRY_SECRET"):
@@ -32,3 +49,121 @@ def log_failure(message: str):
3249
return
3350

3451
logging.warning(message)
52+
53+
54+
async def extract_principal_from_keytab(keytab_file: str) -> str:
55+
"""
56+
Extract principal from the specified keytab file.
57+
Assumes there is a single principal in the keytab.
58+
59+
Args:
60+
keytab_file: Path to a keytab file.
61+
62+
Returns:
63+
Extracted principal name.
64+
"""
65+
proc = await asyncio.create_subprocess_exec(
66+
"klist",
67+
"-k",
68+
"-K",
69+
"-e",
70+
keytab_file,
71+
stdout=subprocess.PIPE,
72+
stderr=subprocess.PIPE,
73+
)
74+
stdout, stderr = await proc.communicate()
75+
if proc.returncode:
76+
logging.error("klist command failed: %s", stderr.decode())
77+
msg = "klist command failed"
78+
raise KerberosError(msg)
79+
80+
# Parse klist output to extract principal
81+
# Format: " 2 principal@REALM (aes256-cts-hmac-sha1-96) (0x...)"
82+
key_pattern = re.compile(r"^\s*(\d+)\s+(\S+)\s+\((\S+)\)\s+\((\S+)\)$")
83+
for line in stdout.decode().splitlines():
84+
if match := key_pattern.match(line):
85+
# Return the principal associated with the first key
86+
return match.group(2)
87+
88+
msg = "No valid key found in the keytab file"
89+
raise KerberosError(msg)
90+
91+
92+
async def init_kerberos_ticket(keytab_file: str) -> str:
93+
"""
94+
Initialize Kerberos ticket from keytab file.
95+
96+
Args:
97+
keytab_file: Path to keytab file
98+
99+
Returns:
100+
Principal name for which ticket was initialized
101+
"""
102+
# Extract principal from keytab
103+
principal = await extract_principal_from_keytab(keytab_file)
104+
logging.debug("Extracted principal from keytab: %s", principal)
105+
106+
# Check if ticket already exists
107+
proc = await asyncio.create_subprocess_exec(
108+
"klist",
109+
"-l",
110+
stdout=subprocess.PIPE,
111+
stderr=subprocess.PIPE,
112+
)
113+
stdout, stderr = await proc.communicate()
114+
115+
if proc.returncode == 0:
116+
# Parse existing principals
117+
principals = [
118+
parts[0]
119+
for line in stdout.decode().splitlines()
120+
if "Expired" not in line
121+
for parts in (line.split(),)
122+
if len(parts) >= 1 and "@" in parts[0]
123+
]
124+
125+
if principal in principals:
126+
logging.info("Using existing Kerberos ticket for %s", principal)
127+
return principal
128+
129+
# Initialize new ticket
130+
logging.info("Initializing Kerberos ticket for %s", principal)
131+
proc = await asyncio.create_subprocess_exec(
132+
"kinit",
133+
"-k",
134+
"-t",
135+
keytab_file,
136+
principal,
137+
stdout=subprocess.PIPE,
138+
stderr=subprocess.PIPE,
139+
)
140+
stdout, stderr = await proc.communicate()
141+
142+
if proc.returncode:
143+
logging.error("kinit failed: %s", stderr.decode())
144+
msg = "kinit command failed"
145+
raise KerberosError(msg)
146+
147+
logging.info("Kerberos ticket initialized for %s", principal)
148+
return principal
149+
150+
151+
async def destroy_kerberos_ticket(principal: str):
152+
"""
153+
Destroy Kerberos ticket for the specified principal.
154+
155+
Args:
156+
principal: Principal name whose ticket should be destroyed
157+
"""
158+
logging.info("Destroying Kerberos ticket for %s", principal)
159+
proc = await asyncio.create_subprocess_exec(
160+
"kdestroy",
161+
"-p",
162+
principal,
163+
stdout=subprocess.PIPE,
164+
stderr=subprocess.PIPE,
165+
)
166+
await proc.communicate()
167+
168+
if proc.returncode:
169+
logging.warning("Failed to destroy Kerberos ticket for %s", principal)

0 commit comments

Comments
 (0)