Skip to content

Commit 34cd855

Browse files
authored
feat: jobmanager token authentication (#587)
* adapt charm * jobmanager_api abstraction WIP checkin * checkin jobmanager api complete * adapt JobManagerPlatform.build * first lint * fix integration test * lint * set status type to str * cleanup * lint * fix token change flush when reusing same jobmanager token * create api client on each request * address pr review * remove comment * address review comment * remove comment * remove SelfHostedRunnerLabel class * remove unused UnsupportedArch code * use url from jobmanager api * use if else for label * use jobmanager api stub in unit test * add jobmanager provider stub * describe DCO020, DCO030, DCO050
1 parent 4b71c7b commit 34cd855

22 files changed

+771
-342
lines changed

github-runner-manager/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ select = ["E", "W", "F", "C", "N", "R", "D", "H"]
8080
# Ignore D107 Missing docstring in __init__
8181
ignore = ["W503", "D107", "E203"]
8282
# D100, D101, D102, D103, D104: Ignore docstring style issues in tests
83-
per-file-ignores = ["tests/*:D100,D101,D102,D103,D104,D205,D212"]
83+
# DCO020, DCO030, DCO050: Ignore docstring argument,returns,raises sections in tests
84+
per-file-ignores = ["tests/*:D100,D101,D102,D103,D104,D205,D212, DCO020, DCO030, DCO050"]
8485
docstring-convention = "google"
8586
# Check for properly formatted copyright header in each file
8687
copyright-check = "True"

github-runner-manager/src/github_runner_manager/configuration/jobmanager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class JobManagerConfiguration(BaseModel):
1010
1111
Attributes:
1212
url: Base url of the job manager API.
13+
token: Token to authenticate with the job manager API.
1314
"""
1415

1516
url: HttpUrl
17+
token: str
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Copyright 2025 Canonical Ltd.
2+
# See LICENSE file for licensing details.
3+
4+
"""Module containing logic to handle calls to the jobmanager api."""
5+
from enum import Enum
6+
7+
import jobmanager_client
8+
from jobmanager_client.exceptions import ApiException, NotFoundException
9+
from pydantic import BaseModel
10+
from urllib3.exceptions import RequestError
11+
12+
13+
class JobManagerAPIError(Exception):
14+
"""Base exception for JobManager API errors."""
15+
16+
17+
class JobManagerAPINotFoundError(JobManagerAPIError):
18+
"""Exception raised when a runner is not found in the JobManager API."""
19+
20+
21+
class JobStatus(str, Enum):
22+
"""Status of a job on the JobManager.
23+
24+
Attributes:
25+
IN_PROGRESS: Represents a job that is in progress.
26+
PENDING: Represents a job that is pending.
27+
"""
28+
29+
IN_PROGRESS = "IN_PROGRESS"
30+
PENDING = "PENDING"
31+
32+
33+
class Job(BaseModel):
34+
"""Represents a job on the JobManagerAPI.
35+
36+
Attributes:
37+
status: The status of the job.
38+
"""
39+
40+
status: str | None
41+
42+
43+
class RunnerStatus(str, Enum):
44+
"""Status of a runner on the JobManager.
45+
46+
Attributes:
47+
IN_PROGRESS: Represents a runner that is in progress.
48+
PENDING: Represents a runner that is pending.
49+
"""
50+
51+
IN_PROGRESS = "IN_PROGRESS"
52+
PENDING = "PENDING"
53+
54+
55+
class RunnerRegistration(BaseModel):
56+
"""Represents a runner registration response from the JobManagerAPI.
57+
58+
Attributes:
59+
id: The ID of the registered runner.
60+
token: The token for the registered runner.
61+
"""
62+
63+
id: int
64+
token: str
65+
66+
67+
class RunnerHealth(BaseModel):
68+
"""Represents the health status of a runner on the JobManagerAPI.
69+
70+
Attributes:
71+
status: The health status of the runner.
72+
deletable: Indicates if the runner can be deleted.
73+
"""
74+
75+
status: str
76+
deletable: bool
77+
78+
79+
class JobManagerAPI:
80+
"""Handles interactions with the JobManager API."""
81+
82+
# The job manager api uses an autogenerated api client that uses urllib3 connection pools
83+
# that are not multiprocessing safe: https://github.com/urllib3/urllib3/issues/850
84+
# Therefore, we create a new ApiClient for each request and close resources
85+
# to avoid issues with multiprocessing in the application.
86+
87+
def __init__(self, token: str, url: str):
88+
"""Initialize the JobManagerAPI with a token and URL.
89+
90+
Args:
91+
token: The authentication token for the JobManager API.
92+
url: The base URL for the JobManager API.
93+
"""
94+
self._token = token
95+
self.url = url
96+
97+
def get_runner_health(self, runner_id: int) -> RunnerHealth:
98+
"""Fetch the health status of a runner by its ID from the JobManager API.
99+
100+
Args:
101+
runner_id: The ID of the runner to fetch health status for.
102+
103+
Raises:
104+
JobManagerAPINotFoundError: If the runner with the given ID is not found.
105+
JobManagerAPIError: If there is an error fetching the runner health.
106+
107+
Returns:
108+
RunnerHealth: The health status of the runner.
109+
"""
110+
with self._create_api_client() as api_client:
111+
runners_api = jobmanager_client.RunnersApi(api_client=api_client)
112+
try:
113+
response = runners_api.get_runner_health_v1_runners_runner_id_health_get(runner_id)
114+
except NotFoundException as err:
115+
raise JobManagerAPINotFoundError(
116+
f"Health for runner with ID {runner_id} not found in JobManager API."
117+
) from err
118+
except (ApiException, RequestError, ValueError) as exc:
119+
raise JobManagerAPIError(
120+
f"Error fetching runner health for ID {runner_id}: {exc}"
121+
) from exc
122+
return RunnerHealth(status=response.status, deletable=response.deletable)
123+
124+
def register_runner(self, name: str, labels: list[str]) -> RunnerRegistration:
125+
"""Register a new runner with the JobManager API.
126+
127+
Args:
128+
name: The name of the runner to register.
129+
labels: A list of labels to associate with the runner.
130+
131+
Returns:
132+
RunnerRegistration: The registration details of the runner, including ID and token.
133+
134+
Raises:
135+
JobManagerAPIError: If there is an error registering the runner.
136+
"""
137+
with self._create_api_client() as api_client:
138+
runners_api = jobmanager_client.RunnersApi(api_client=api_client)
139+
runner_register_request = jobmanager_client.RunnerCreate(name=name, labels=labels)
140+
141+
try:
142+
response = runners_api.register_runner_v1_runners_register_post(
143+
runner_register_request
144+
)
145+
except (ApiException, RequestError, ValueError) as exc:
146+
raise JobManagerAPIError(f"Error registering runner: {exc}") from exc
147+
return RunnerRegistration(id=response.id, token=response.token)
148+
149+
def get_job(self, job_id: int) -> Job:
150+
"""Fetch a job by its ID from the JobManager API.
151+
152+
Args:
153+
job_id: The ID of the job to fetch.
154+
155+
Returns:
156+
Job: The job object containing its status.
157+
158+
Raises:
159+
JobManagerAPINotFoundError: If the job with the given ID is not found.
160+
JobManagerAPIError: If there is an error fetching the job.
161+
"""
162+
with self._create_api_client() as api_client:
163+
jobs_api = jobmanager_client.JobsApi(api_client=api_client)
164+
try:
165+
response = jobs_api.get_job_v1_jobs_job_id_get(job_id)
166+
except NotFoundException as err:
167+
raise JobManagerAPINotFoundError(
168+
f"Job with ID {job_id} not found in JobManager API."
169+
) from err
170+
except (ApiException, RequestError, ValueError) as exc:
171+
raise JobManagerAPIError(f"Error fetching job with ID {job_id}: {exc}") from exc
172+
return Job(status=response.status)
173+
174+
def _create_api_client(self) -> jobmanager_client.ApiClient:
175+
"""Create a new API client for the JobManager API.
176+
177+
Returns:
178+
jobmanager_client.ApiClient: A new API client configured with the JobManager
179+
API URL and token.
180+
"""
181+
config = jobmanager_client.Configuration(host=self.url)
182+
api_client = jobmanager_client.ApiClient(configuration=config)
183+
api_client.set_default_header("Authorization", f"Bearer {self._token}")
184+
return api_client

0 commit comments

Comments
 (0)