Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cca2d7f
adapt charm
cbartz Jul 1, 2025
b2faf78
jobmanager_api abstraction WIP checkin
cbartz Jul 1, 2025
a52a737
checkin jobmanager api complete
cbartz Jul 1, 2025
6bd4fae
adapt JobManagerPlatform.build
cbartz Jul 1, 2025
59d492f
first lint
cbartz Jul 1, 2025
3580c62
fix integration test
cbartz Jul 2, 2025
999939a
lint
cbartz Jul 2, 2025
fa1882c
set status type to str
cbartz Jul 2, 2025
2ef696e
cleanup
cbartz Jul 2, 2025
3db4fdc
lint
cbartz Jul 2, 2025
7e3ebad
fix token change flush when reusing same jobmanager token
cbartz Jul 2, 2025
a5ef508
create api client on each request
cbartz Jul 3, 2025
648973e
Merge branch 'main' into feat/jobmanager-token-auth-ISD-3752
cbartz Jul 9, 2025
b4b3db7
address pr review
cbartz Jul 9, 2025
7307332
remove comment
cbartz Jul 9, 2025
3cb3a53
address review comment
cbartz Jul 9, 2025
c8f90fc
remove comment
cbartz Jul 9, 2025
edded67
remove SelfHostedRunnerLabel class
cbartz Jul 9, 2025
2d4ef00
Merge branch 'main' into feat/jobmanager-token-auth-ISD-3752
cbartz Jul 9, 2025
f47f79a
Merge branch 'main' into feat/jobmanager-token-auth-ISD-3752
cbartz Jul 9, 2025
9c7b571
remove unused UnsupportedArch code
cbartz Jul 14, 2025
dfd765e
Merge branch 'main' into feat/jobmanager-token-auth-ISD-3752
cbartz Jul 14, 2025
7fc51ca
use url from jobmanager api
cbartz Jul 14, 2025
f41959b
use if else for label
cbartz Jul 14, 2025
3e658fb
use jobmanager api stub in unit test
cbartz Jul 14, 2025
5bf0d5e
add jobmanager provider stub
cbartz Jul 14, 2025
e3e821e
Merge branch 'main' into feat/jobmanager-token-auth-ISD-3752
cbartz Jul 14, 2025
7b84c53
describe DCO020, DCO030, DCO050
cbartz Jul 15, 2025
27a9bda
Merge remote-tracking branch 'origin/feat/jobmanager-token-auth-ISD-3…
cbartz Jul 15, 2025
9bef95b
Merge branch 'main' into feat/jobmanager-token-auth-ISD-3752
cbartz Jul 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion github-runner-manager/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ select = ["E", "W", "F", "C", "N", "R", "D", "H"]
# Ignore D107 Missing docstring in __init__
ignore = ["W503", "D107", "E203"]
# D100, D101, D102, D103, D104: Ignore docstring style issues in tests
per-file-ignores = ["tests/*:D100,D101,D102,D103,D104,D205,D212"]
# DCO020, DCO030, DCO050: Ignore docstring argument,returns,raises sections in tests
per-file-ignores = ["tests/*:D100,D101,D102,D103,D104,D205,D212, DCO020, DCO030, DCO050"]
Comment thread
cbartz marked this conversation as resolved.
docstring-convention = "google"
# Check for properly formatted copyright header in each file
copyright-check = "True"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class JobManagerConfiguration(BaseModel):

Attributes:
url: Base url of the job manager API.
token: Token to authenticate with the job manager API.
"""

url: HttpUrl
token: str
184 changes: 184 additions & 0 deletions github-runner-manager/src/github_runner_manager/jobmanager_api.py
Comment thread
cbartz marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Copyright 2025 Canonical Ltd.
Comment thread
yanksyoon marked this conversation as resolved.
# See LICENSE file for licensing details.

"""Module containing logic to handle calls to the jobmanager api."""
from enum import Enum

import jobmanager_client
from jobmanager_client.exceptions import ApiException, NotFoundException
from pydantic import BaseModel
from urllib3.exceptions import RequestError


class JobManagerAPIError(Exception):
"""Base exception for JobManager API errors."""


class JobManagerAPINotFoundError(JobManagerAPIError):
Comment thread
yhaliaw marked this conversation as resolved.
"""Exception raised when a runner is not found in the JobManager API."""


class JobStatus(str, Enum):
Comment thread
yanksyoon marked this conversation as resolved.
"""Status of a job on the JobManager.

Attributes:
IN_PROGRESS: Represents a job that is in progress.
PENDING: Represents a job that is pending.
"""

IN_PROGRESS = "IN_PROGRESS"
PENDING = "PENDING"


class Job(BaseModel):
"""Represents a job on the JobManagerAPI.

Attributes:
status: The status of the job.
"""

status: str | None
Comment thread
yanksyoon marked this conversation as resolved.


class RunnerStatus(str, Enum):
"""Status of a runner on the JobManager.

Attributes:
IN_PROGRESS: Represents a runner that is in progress.
PENDING: Represents a runner that is pending.
"""

IN_PROGRESS = "IN_PROGRESS"
PENDING = "PENDING"


class RunnerRegistration(BaseModel):
"""Represents a runner registration response from the JobManagerAPI.

Attributes:
id: The ID of the registered runner.
token: The token for the registered runner.
"""

id: int
token: str


class RunnerHealth(BaseModel):
"""Represents the health status of a runner on the JobManagerAPI.

Attributes:
status: The health status of the runner.
deletable: Indicates if the runner can be deleted.
"""

status: str
Comment thread
yanksyoon marked this conversation as resolved.
deletable: bool


class JobManagerAPI:
"""Handles interactions with the JobManager API."""

# The job manager api uses an autogenerated api client that uses urllib3 connection pools
# that are not multiprocessing safe: https://github.com/urllib3/urllib3/issues/850
# Therefore, we create a new ApiClient for each request and close resources
# to avoid issues with multiprocessing in the application.

def __init__(self, token: str, url: str):
"""Initialize the JobManagerAPI with a token and URL.

Args:
token: The authentication token for the JobManager API.
url: The base URL for the JobManager API.
"""
self._token = token
self.url = url

def get_runner_health(self, runner_id: int) -> RunnerHealth:
"""Fetch the health status of a runner by its ID from the JobManager API.

Args:
runner_id: The ID of the runner to fetch health status for.

Raises:
JobManagerAPINotFoundError: If the runner with the given ID is not found.
JobManagerAPIError: If there is an error fetching the runner health.

Returns:
RunnerHealth: The health status of the runner.
"""
with self._create_api_client() as api_client:
runners_api = jobmanager_client.RunnersApi(api_client=api_client)
try:
response = runners_api.get_runner_health_v1_runners_runner_id_health_get(runner_id)
except NotFoundException as err:
raise JobManagerAPINotFoundError(
f"Health for runner with ID {runner_id} not found in JobManager API."
) from err
except (ApiException, RequestError, ValueError) as exc:
raise JobManagerAPIError(
f"Error fetching runner health for ID {runner_id}: {exc}"
) from exc
return RunnerHealth(status=response.status, deletable=response.deletable)

def register_runner(self, name: str, labels: list[str]) -> RunnerRegistration:
"""Register a new runner with the JobManager API.

Args:
name: The name of the runner to register.
labels: A list of labels to associate with the runner.

Returns:
RunnerRegistration: The registration details of the runner, including ID and token.

Raises:
JobManagerAPIError: If there is an error registering the runner.
"""
with self._create_api_client() as api_client:
runners_api = jobmanager_client.RunnersApi(api_client=api_client)
runner_register_request = jobmanager_client.RunnerCreate(name=name, labels=labels)

try:
response = runners_api.register_runner_v1_runners_register_post(
runner_register_request
)
except (ApiException, RequestError, ValueError) as exc:
raise JobManagerAPIError(f"Error registering runner: {exc}") from exc
return RunnerRegistration(id=response.id, token=response.token)

def get_job(self, job_id: int) -> Job:
"""Fetch a job by its ID from the JobManager API.

Args:
job_id: The ID of the job to fetch.

Returns:
Job: The job object containing its status.

Raises:
JobManagerAPINotFoundError: If the job with the given ID is not found.
JobManagerAPIError: If there is an error fetching the job.
"""
with self._create_api_client() as api_client:
jobs_api = jobmanager_client.JobsApi(api_client=api_client)
try:
response = jobs_api.get_job_v1_jobs_job_id_get(job_id)
except NotFoundException as err:
Comment thread
yanksyoon marked this conversation as resolved.
raise JobManagerAPINotFoundError(
f"Job with ID {job_id} not found in JobManager API."
) from err
except (ApiException, RequestError, ValueError) as exc:
raise JobManagerAPIError(f"Error fetching job with ID {job_id}: {exc}") from exc
return Job(status=response.status)

def _create_api_client(self) -> jobmanager_client.ApiClient:
"""Create a new API client for the JobManager API.

Returns:
jobmanager_client.ApiClient: A new API client configured with the JobManager
API URL and token.
"""
config = jobmanager_client.Configuration(host=self.url)
api_client = jobmanager_client.ApiClient(configuration=config)
api_client.set_default_header("Authorization", f"Bearer {self._token}")
return api_client
Loading
Loading