Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PTFE-698 Add fastapi dependency for GitHub client #337

Merged
merged 14 commits into from
Aug 14, 2023
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"github.vscode-github-actions",
"ms-kubernetes-tools.vscode-kubernetes-tools",
"ms-python.python",
"ms-python.vscode-pylance"
"ms-python.vscode-pylance",
"GitHub.copilot-chat"
],
"settings": {
"python": {
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ jobs:
- name: get all resources
if: failure()
run: kubectl get all
- name: describe all resources
if: failure()
run: kubectl describe all
- name: get logs
if: failure()
run: |
Expand Down
2 changes: 1 addition & 1 deletion manifests/base/runner-manager/main/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ spec:
livenessProbe:
failureThreshold: 3
httpGet:
path: /_health
path: /_health/
port: http
initialDelaySeconds: 5
periodSeconds: 10
Expand Down
17 changes: 17 additions & 0 deletions runner_manager/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from functools import lru_cache

import httpx
from githubkit.config import Config
from redis import Redis
from redis_om import get_redis_connection
from rq import Queue

from runner_manager.clients.github import GitHub
from runner_manager.models.settings import Settings


Expand All @@ -17,5 +20,19 @@ def get_redis() -> Redis:
return get_redis_connection(url=get_settings().redis_om_url, decode_responses=True)


@lru_cache
def get_queue() -> Queue:
return Queue(connection=get_redis())


@lru_cache()
def get_github() -> GitHub:
settings: Settings = get_settings()
config: Config = Config(
base_url=httpx.URL(str(settings.github_base_url)),
follow_redirects=True,
accept="*/*",
user_agent="runner-manager",
timeout=httpx.Timeout(30.0),
)
return GitHub(settings.github_auth_strategy(), config=config)
53 changes: 50 additions & 3 deletions runner_manager/models/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from typing import Any, Dict, List, Optional, Sequence

import yaml
from pydantic import AnyHttpUrl, BaseSettings, RedisDsn, SecretStr
from githubkit import AppInstallationAuthStrategy, TokenAuthStrategy
from pydantic import AnyHttpUrl, BaseSettings, ConfigError, RedisDsn, SecretStr

from runner_manager.models.runner_group import BaseRunnerGroup

Expand Down Expand Up @@ -34,16 +35,62 @@ class LogLevel(str, Enum):
class Settings(BaseSettings):
name: str = "runner-manager"
redis_om_url: Optional[RedisDsn] = None
github_base_url: Optional[AnyHttpUrl] = None
api_key: Optional[SecretStr] = None
allowed_hosts: Optional[Sequence[str]] = [
"localhost",
"127.0.0.1",
"testserver",
"10.*",
]
tcarmet marked this conversation as resolved.
Show resolved Hide resolved
github_webhook_secret: Optional[SecretStr] = None
log_level: LogLevel = LogLevel.INFO
runner_groups: List[BaseRunnerGroup] = []

github_base_url: AnyHttpUrl = AnyHttpUrl("api.github.com", scheme="https")
github_webhook_secret: Optional[SecretStr] = None
github_token: Optional[SecretStr] = None
github_app_id: int | str = 0
github_private_key: SecretStr = SecretStr("")
github_installation_id: int = 0
github_client_id: Optional[str] = None
github_client_secret: SecretStr = SecretStr("")

@property
def app_install(self) -> bool:
"""
Returns True if the github auth strategy should be an app install.

To consider an app install user should define:

- github_app_id
- github_private_key
- github_installation_id
"""
if (
self.github_app_id
and self.github_private_key
and self.github_installation_id
):
return True
return False

def github_auth_strategy(self) -> AppInstallationAuthStrategy | TokenAuthStrategy:
"""
Returns the appropriate auth strategy for the current configuration.
"""
# prefer AppInstallationAuthStrategy over TokenAuthStrategy
if self.app_install:
return AppInstallationAuthStrategy(
app_id=self.github_app_id,
installation_id=self.github_installation_id,
private_key=self.github_private_key.get_secret_value(),
client_id=self.github_client_id,
client_secret=self.github_client_secret.get_secret_value(),
)
elif self.github_token:
return TokenAuthStrategy(token=self.github_token.get_secret_value())
else:
raise ConfigError("No github auth strategy configured")

class Config:
smart_union = True
env_file = ".env"
Expand Down
12 changes: 12 additions & 0 deletions tests/api/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from datetime import timedelta
from functools import lru_cache

import pytest
from fastapi.testclient import TestClient
from hypothesis import HealthCheck, settings

from runner_manager import Settings
from runner_manager.dependencies import get_settings
from runner_manager.main import app

settings.register_profile(
Expand All @@ -16,10 +19,19 @@
settings.load_profile("api")


@lru_cache()
def api_settings():
return Settings(
github_token="test",
github_base_url="http://localhost:4010",
)


@pytest.fixture(scope="function")
def fastapp():
fastapp = app
fastapp.dependency_overrides = {}
fastapp.dependency_overrides[get_settings] = api_settings
return fastapp


Expand Down
26 changes: 26 additions & 0 deletions tests/api/test_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import List

from fastapi import APIRouter, Depends
from githubkit import Response
from githubkit.rest import OrgsOrgActionsRunnersGetResponse200, Runner

from runner_manager.clients.github import GitHub
from runner_manager.dependencies import get_github

router = APIRouter()


@router.get("/runners")
def list_runners(github: GitHub = Depends(get_github)) -> List[Runner]:
resp: Response[
List[OrgsOrgActionsRunnersGetResponse200]
] = github.rest.actions.list_self_hosted_runners_for_org(org="octo-org")

return resp.parsed_data.runners


def test_github_dependency(client, fastapp):
fastapp.include_router(router, prefix="/api")
# fastapp.dependency_overrides[get_settings] = settings
runners: List[Runner] = client.get("/runners").json()
assert len(runners) >= 1
48 changes: 48 additions & 0 deletions tests/unit/models/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

import pytest
import yaml
from githubkit import AppInstallationAuthStrategy, TokenAuthStrategy
from hypothesis import assume, given
from hypothesis import strategies as st
from pydantic import ConfigError
from pytest import fixture

from runner_manager.dependencies import get_settings
Expand Down Expand Up @@ -73,6 +77,50 @@ def test_get_settings(config_file):
Settings()


@given(
st.builds(
Settings,
github_app_id=st.integers(),
github_installation_id=st.integers(),
github_private_key=st.text(),
github_token=st.text(),
)
)
def test_app_install(stsettings):
assume(stsettings.github_app_id)
assume(stsettings.github_installation_id)
assume(stsettings.github_private_key)
assert stsettings.app_install is True
assert isinstance(stsettings.github_auth_strategy(), AppInstallationAuthStrategy)


@given(
st.builds(
Settings,
github_app_id=st.just(0),
github_installation_id=st.just(0),
github_private_key=st.just(""),
)
)
def test_token_auth_strategy(stsettings):
if stsettings.github_token:
assert isinstance(stsettings.github_auth_strategy(), TokenAuthStrategy)


@given(
st.builds(
Settings,
github_token=st.none(),
github_app_id=st.just(0),
github_installation_id=st.just(0),
github_private_key=st.just(""),
)
)
def test_config_error(stsettings):
with pytest.raises(ConfigError):
stsettings.github_auth_strategy()


def test_settings_runner_group(runner_group: RunnerGroup):
settings = Settings(runner_groups=[runner_group])
assert settings.runner_groups == [runner_group]
Loading