Utilize pydantic
for serialization and deserialization
Signed-off-by: magic_rb <richard@brezak.sk>
This commit is contained in:
parent
9086472a5f
commit
6e8e735628
|
@ -5,7 +5,6 @@ import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
@ -33,13 +32,12 @@ from twisted.logger import Logger
|
||||||
from .common import (
|
from .common import (
|
||||||
slugify_project_name,
|
slugify_project_name,
|
||||||
)
|
)
|
||||||
from .gitea_projects import GiteaBackend, GiteaConfig
|
from .gitea_projects import GiteaBackend
|
||||||
from .github_projects import (
|
from .github_projects import (
|
||||||
GithubBackend,
|
GithubBackend,
|
||||||
GithubConfig,
|
|
||||||
)
|
)
|
||||||
|
from .models import BuildbotNixConfig
|
||||||
from .projects import GitBackend, GitProject
|
from .projects import GitBackend, GitProject
|
||||||
from .secrets import read_secret_file
|
|
||||||
|
|
||||||
SKIPPED_BUILDER_NAME = "skipped-builds"
|
SKIPPED_BUILDER_NAME = "skipped-builds"
|
||||||
|
|
||||||
|
@ -428,21 +426,6 @@ def nix_eval_config(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CachixConfig:
|
|
||||||
name: str
|
|
||||||
signing_key_secret_name: str | None = None
|
|
||||||
auth_token_secret_name: str | None = None
|
|
||||||
|
|
||||||
def cachix_env(self) -> dict[str, str]:
|
|
||||||
env = {}
|
|
||||||
if self.signing_key_secret_name is not None:
|
|
||||||
env["CACHIX_SIGNING_KEY"] = util.Secret(self.signing_key_secret_name)
|
|
||||||
if self.auth_token_secret_name is not None:
|
|
||||||
env["CACHIX_AUTH_TOKEN"] = util.Secret(self.auth_token_secret_name)
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def do_register_gcroot_if(s: steps.BuildStep) -> Generator[Any, object, Any]:
|
def do_register_gcroot_if(s: steps.BuildStep) -> Generator[Any, object, Any]:
|
||||||
gc_root = yield util.Interpolate(
|
gc_root = yield util.Interpolate(
|
||||||
|
@ -847,53 +830,23 @@ class PeriodicWithStartup(schedulers.Periodic):
|
||||||
class NixConfigurator(ConfiguratorBase):
|
class NixConfigurator(ConfiguratorBase):
|
||||||
"""Janitor is a configurator which create a Janitor Builder with all needed Janitor steps"""
|
"""Janitor is a configurator which create a Janitor Builder with all needed Janitor steps"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, config: BuildbotNixConfig) -> None:
|
||||||
self,
|
|
||||||
# Shape of this file: [ { "name": "<worker-name>", "pass": "<worker-password>", "cores": "<cpu-cores>" } ]
|
|
||||||
admins: list[str],
|
|
||||||
auth_backend: str,
|
|
||||||
build_retries: int,
|
|
||||||
github: GithubConfig | None,
|
|
||||||
gitea: GiteaConfig | None,
|
|
||||||
url: str,
|
|
||||||
nix_supported_systems: list[str],
|
|
||||||
nix_eval_worker_count: int | None,
|
|
||||||
nix_eval_max_memory_size: int,
|
|
||||||
post_build_steps: list[steps.BuildStep] | None = None,
|
|
||||||
nix_workers_secret_name: str = "buildbot-nix-workers", # noqa: S107
|
|
||||||
cachix: CachixConfig | None = None,
|
|
||||||
outputs_path: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.nix_workers_secret_name = nix_workers_secret_name
|
|
||||||
self.nix_eval_max_memory_size = nix_eval_max_memory_size
|
self.config = config
|
||||||
self.nix_eval_worker_count = nix_eval_worker_count
|
|
||||||
self.nix_supported_systems = nix_supported_systems
|
|
||||||
self.post_build_steps = post_build_steps or []
|
|
||||||
self.auth_backend = auth_backend
|
|
||||||
self.admins = admins
|
|
||||||
self.github = github
|
|
||||||
self.gitea = gitea
|
|
||||||
self.url = url
|
|
||||||
self.cachix = cachix
|
|
||||||
self.build_retries = build_retries
|
|
||||||
if outputs_path is None:
|
|
||||||
self.outputs_path = None
|
|
||||||
else:
|
|
||||||
self.outputs_path = Path(outputs_path)
|
|
||||||
|
|
||||||
def configure(self, config: dict[str, Any]) -> None:
|
def configure(self, config: dict[str, Any]) -> None:
|
||||||
backends: dict[str, GitBackend] = {}
|
backends: dict[str, GitBackend] = {}
|
||||||
|
|
||||||
if self.github is not None:
|
if self.config.github is not None:
|
||||||
backends["github"] = GithubBackend(self.github, self.url)
|
backends["github"] = GithubBackend(self.config.github, self.config.url)
|
||||||
|
|
||||||
if self.gitea is not None:
|
if self.config.gitea is not None:
|
||||||
backends["gitea"] = GiteaBackend(self.gitea)
|
backends["gitea"] = GiteaBackend(self.config.gitea)
|
||||||
|
|
||||||
auth: AuthBase | None = (
|
auth: AuthBase | None = (
|
||||||
backends[self.auth_backend].create_auth()
|
backends[self.config.auth_backend].create_auth()
|
||||||
if self.auth_backend != "none"
|
if self.config.auth_backend != "none"
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -902,7 +855,7 @@ class NixConfigurator(ConfiguratorBase):
|
||||||
for backend in backends.values():
|
for backend in backends.values():
|
||||||
projects += backend.load_projects()
|
projects += backend.load_projects()
|
||||||
|
|
||||||
worker_config = json.loads(read_secret_file(self.nix_workers_secret_name))
|
worker_config = json.loads(self.config.nix_workers_secret)
|
||||||
worker_names = []
|
worker_names = []
|
||||||
|
|
||||||
config.setdefault("projects", [])
|
config.setdefault("projects", [])
|
||||||
|
@ -918,7 +871,7 @@ class NixConfigurator(ConfiguratorBase):
|
||||||
|
|
||||||
eval_lock = util.MasterLock("nix-eval")
|
eval_lock = util.MasterLock("nix-eval")
|
||||||
|
|
||||||
if self.cachix is not None:
|
if self.config.cachix is not None:
|
||||||
self.post_build_steps.append(
|
self.post_build_steps.append(
|
||||||
steps.ShellCommand(
|
steps.ShellCommand(
|
||||||
name="Upload cachix",
|
name="Upload cachix",
|
||||||
|
@ -937,13 +890,13 @@ class NixConfigurator(ConfiguratorBase):
|
||||||
config,
|
config,
|
||||||
project,
|
project,
|
||||||
worker_names,
|
worker_names,
|
||||||
self.nix_supported_systems,
|
self.config.build_systems,
|
||||||
self.nix_eval_worker_count or multiprocessing.cpu_count(),
|
self.config.eval_worker_count or multiprocessing.cpu_count(),
|
||||||
self.nix_eval_max_memory_size,
|
self.config.eval_max_memory_size,
|
||||||
eval_lock,
|
eval_lock,
|
||||||
self.post_build_steps,
|
[x.to_buildstep() for x in self.config.post_build_steps],
|
||||||
self.outputs_path,
|
self.config.outputs_path,
|
||||||
self.build_retries,
|
self.config.build_retries,
|
||||||
)
|
)
|
||||||
|
|
||||||
config["workers"].append(worker.LocalWorker(SKIPPED_BUILDER_NAME))
|
config["workers"].append(worker.LocalWorker(SKIPPED_BUILDER_NAME))
|
||||||
|
@ -998,7 +951,7 @@ class NixConfigurator(ConfiguratorBase):
|
||||||
config["www"]["auth"] = auth
|
config["www"]["auth"] = auth
|
||||||
|
|
||||||
config["www"]["authz"] = setup_authz(
|
config["www"]["authz"] = setup_authz(
|
||||||
admins=self.admins,
|
admins=self.config.admins,
|
||||||
backends=list(backends.values()),
|
backends=list(backends.values()),
|
||||||
projects=projects,
|
projects=projects,
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,10 +6,12 @@ from abc import ABC, abstractmethod
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any, TypeVar
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from buildbot.process.log import StreamLog
|
from buildbot.process.log import StreamLog
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
||||||
from buildbot.plugins import util
|
from buildbot.plugins import util
|
||||||
|
@ -110,9 +112,12 @@ def atomic_write_file(file: Path, data: str) -> None:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
Y = TypeVar("Y")
|
||||||
|
|
||||||
|
|
||||||
def filter_repos_by_topic(
|
def filter_repos_by_topic(
|
||||||
topic: str | None, repos: list[Any], topics: Callable[[Any], list[str]]
|
topic: str | None, repos: list[Y], topics: Callable[[Y], list[str]]
|
||||||
) -> list[Any]:
|
) -> list[Y]:
|
||||||
return list(
|
return list(
|
||||||
filter(
|
filter(
|
||||||
lambda repo: topic is None or topic in topics(repo),
|
lambda repo: topic is None or topic in topics(repo),
|
||||||
|
@ -154,3 +159,16 @@ class ThreadDeferredBuildStep(BuildStep, ABC):
|
||||||
log: StreamLog = yield self.addLog("log")
|
log: StreamLog = yield self.addLog("log")
|
||||||
log.addStderr(f"Failed to reload project list: {self.error_msg}")
|
log.addStderr(f"Failed to reload project list: {self.error_msg}")
|
||||||
return util.FAILURE
|
return util.FAILURE
|
||||||
|
|
||||||
|
|
||||||
|
_T = TypeVar("_T", bound="BaseModel")
|
||||||
|
|
||||||
|
|
||||||
|
def model_validate_project_cache(cls: type[_T], project_cache_file: Path) -> list[_T]:
|
||||||
|
return [
|
||||||
|
cls.model_validate(data) for data in json.loads(project_cache_file.read_text())
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def model_dump_project_cache(repos: list[_T]) -> str:
|
||||||
|
return json.dumps([repo.model_dump() for repo in repos])
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
@ -14,6 +12,7 @@ from buildbot.www.auth import AuthBase
|
||||||
from buildbot.www.avatar import AvatarBase
|
from buildbot.www.avatar import AvatarBase
|
||||||
from buildbot_gitea.auth import GiteaAuth # type: ignore[import]
|
from buildbot_gitea.auth import GiteaAuth # type: ignore[import]
|
||||||
from buildbot_gitea.reporter import GiteaStatusPush # type: ignore[import]
|
from buildbot_gitea.reporter import GiteaStatusPush # type: ignore[import]
|
||||||
|
from pydantic import BaseModel
|
||||||
from twisted.logger import Logger
|
from twisted.logger import Logger
|
||||||
from twisted.python import log
|
from twisted.python import log
|
||||||
|
|
||||||
|
@ -22,40 +21,37 @@ from .common import (
|
||||||
atomic_write_file,
|
atomic_write_file,
|
||||||
filter_repos_by_topic,
|
filter_repos_by_topic,
|
||||||
http_request,
|
http_request,
|
||||||
|
model_dump_project_cache,
|
||||||
|
model_validate_project_cache,
|
||||||
paginated_github_request,
|
paginated_github_request,
|
||||||
slugify_project_name,
|
slugify_project_name,
|
||||||
)
|
)
|
||||||
|
from .models import GiteaConfig
|
||||||
from .projects import GitBackend, GitProject
|
from .projects import GitBackend, GitProject
|
||||||
from .secrets import read_secret_file
|
|
||||||
|
|
||||||
tlog = Logger()
|
tlog = Logger()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class RepoOwnerData(BaseModel):
|
||||||
class GiteaConfig:
|
login: str
|
||||||
instance_url: str
|
|
||||||
oauth_id: str | None
|
|
||||||
|
|
||||||
oauth_secret_name: str = "gitea-oauth-secret"
|
|
||||||
token_secret_name: str = "gitea-token"
|
|
||||||
webhook_secret_name: str = "gitea-webhook-secret"
|
|
||||||
project_cache_file: Path = Path("gitea-project-cache.json")
|
|
||||||
topic: str | None = "build-with-buildbot"
|
|
||||||
|
|
||||||
def oauth_secret(self) -> str:
|
class RepoData(BaseModel):
|
||||||
return read_secret_file(self.oauth_secret_name)
|
name: str
|
||||||
|
owner: RepoOwnerData
|
||||||
def token(self) -> str:
|
full_name: str
|
||||||
return read_secret_file(self.token_secret_name)
|
ssh_url: str
|
||||||
|
default_branch: str
|
||||||
|
topics: list[str]
|
||||||
|
|
||||||
|
|
||||||
class GiteaProject(GitProject):
|
class GiteaProject(GitProject):
|
||||||
config: GiteaConfig
|
config: GiteaConfig
|
||||||
webhook_secret: str
|
webhook_secret: str
|
||||||
data: dict[str, Any]
|
data: RepoData
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, config: GiteaConfig, webhook_secret: str, data: dict[str, Any]
|
self, config: GiteaConfig, webhook_secret: str, data: RepoData
|
||||||
) -> None:
|
) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.webhook_secret = webhook_secret
|
self.webhook_secret = webhook_secret
|
||||||
|
@ -63,7 +59,7 @@ class GiteaProject(GitProject):
|
||||||
|
|
||||||
def get_project_url(self) -> str:
|
def get_project_url(self) -> str:
|
||||||
url = urlparse(self.config.instance_url)
|
url = urlparse(self.config.instance_url)
|
||||||
return f"{url.scheme}://git:%(secret:{self.config.token_secret_name})s@{url.hostname}/{self.name}"
|
return f"{url.scheme}://git:%(secret:{self.config.token_file})s@{url.hostname}/{self.name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pretty_type(self) -> str:
|
def pretty_type(self) -> str:
|
||||||
|
@ -75,33 +71,33 @@ class GiteaProject(GitProject):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repo(self) -> str:
|
def repo(self) -> str:
|
||||||
return self.data["name"]
|
return self.data.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def owner(self) -> str:
|
def owner(self) -> str:
|
||||||
return self.data["owner"]["login"]
|
return self.data.owner.login
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self.data["full_name"]
|
return self.data.full_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> str:
|
def url(self) -> str:
|
||||||
# not `html_url` because https://github.com/lab132/buildbot-gitea/blob/f569a2294ea8501ef3bcc5d5b8c777dfdbf26dcc/buildbot_gitea/webhook.py#L34
|
# not `html_url` because https://github.com/lab132/buildbot-gitea/blob/f569a2294ea8501ef3bcc5d5b8c777dfdbf26dcc/buildbot_gitea/webhook.py#L34
|
||||||
return self.data["ssh_url"]
|
return self.data.ssh_url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project_id(self) -> str:
|
def project_id(self) -> str:
|
||||||
return slugify_project_name(self.data["full_name"])
|
return slugify_project_name(self.data.full_name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_branch(self) -> str:
|
def default_branch(self) -> str:
|
||||||
return self.data["default_branch"]
|
return self.data.default_branch
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def topics(self) -> list[str]:
|
def topics(self) -> list[str]:
|
||||||
# note that Gitea doesn't by default put this data here, we splice it in, in `refresh_projects`
|
# note that Gitea doesn't by default put this data here, we splice it in, in `refresh_projects`
|
||||||
return self.data["topics"]
|
return self.data.topics
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def belongs_to_org(self) -> bool:
|
def belongs_to_org(self) -> bool:
|
||||||
|
@ -111,11 +107,9 @@ class GiteaProject(GitProject):
|
||||||
|
|
||||||
class GiteaBackend(GitBackend):
|
class GiteaBackend(GitBackend):
|
||||||
config: GiteaConfig
|
config: GiteaConfig
|
||||||
webhook_secret: str
|
|
||||||
|
|
||||||
def __init__(self, config: GiteaConfig) -> None:
|
def __init__(self, config: GiteaConfig) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.webhook_secret = read_secret_file(self.config.webhook_secret_name)
|
|
||||||
|
|
||||||
def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
|
def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig:
|
||||||
"""Updates the flake an opens a PR for it."""
|
"""Updates the flake an opens a PR for it."""
|
||||||
|
@ -124,9 +118,7 @@ class GiteaBackend(GitBackend):
|
||||||
ReloadGiteaProjects(self.config, self.config.project_cache_file),
|
ReloadGiteaProjects(self.config, self.config.project_cache_file),
|
||||||
)
|
)
|
||||||
factory.addStep(
|
factory.addStep(
|
||||||
CreateGiteaProjectHooks(
|
CreateGiteaProjectHooks(self.config),
|
||||||
self.config, self.config.project_cache_file, self.webhook_secret
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return util.BuilderConfig(
|
return util.BuilderConfig(
|
||||||
name=self.reload_builder_name,
|
name=self.reload_builder_name,
|
||||||
|
@ -137,14 +129,14 @@ class GiteaBackend(GitBackend):
|
||||||
def create_reporter(self) -> ReporterBase:
|
def create_reporter(self) -> ReporterBase:
|
||||||
return GiteaStatusPush(
|
return GiteaStatusPush(
|
||||||
self.config.instance_url,
|
self.config.instance_url,
|
||||||
Interpolate(self.config.token()),
|
Interpolate(self.config.token),
|
||||||
context=Interpolate("buildbot/%(prop:status_name)s"),
|
context=Interpolate("buildbot/%(prop:status_name)s"),
|
||||||
context_pr=Interpolate("buildbot/%(prop:status_name)s"),
|
context_pr=Interpolate("buildbot/%(prop:status_name)s"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_change_hook(self) -> dict[str, Any]:
|
def create_change_hook(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"secret": self.webhook_secret,
|
"secret": self.config.webhook_secret,
|
||||||
# The "mergable" field is a bit buggy,
|
# The "mergable" field is a bit buggy,
|
||||||
# we already do the merge locally anyway.
|
# we already do the merge locally anyway.
|
||||||
"onlyMergeablePullRequest": False,
|
"onlyMergeablePullRequest": False,
|
||||||
|
@ -158,29 +150,32 @@ class GiteaBackend(GitBackend):
|
||||||
return GiteaAuth(
|
return GiteaAuth(
|
||||||
self.config.instance_url,
|
self.config.instance_url,
|
||||||
self.config.oauth_id,
|
self.config.oauth_id,
|
||||||
self.config.oauth_secret(),
|
self.config.oauth_secret,
|
||||||
)
|
)
|
||||||
|
|
||||||
def load_projects(self) -> list["GitProject"]:
|
def load_projects(self) -> list["GitProject"]:
|
||||||
if not self.config.project_cache_file.exists():
|
if not self.config.project_cache_file.exists():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
repos: list[dict[str, Any]] = filter_repos_by_topic(
|
repos: list[RepoData] = filter_repos_by_topic(
|
||||||
self.config.topic,
|
self.config.topic,
|
||||||
sorted(
|
sorted(
|
||||||
json.loads(self.config.project_cache_file.read_text()),
|
model_validate_project_cache(RepoData, self.config.project_cache_file),
|
||||||
key=lambda x: x["full_name"],
|
key=lambda repo: repo.full_name,
|
||||||
),
|
),
|
||||||
lambda repo: repo["topics"],
|
lambda repo: repo.topics,
|
||||||
)
|
)
|
||||||
repo_names: list[str] = [
|
repo_names: list[str] = [repo.owner.login + "/" + repo.name for repo in repos]
|
||||||
repo["owner"]["login"] + "/" + repo["name"] for repo in repos
|
|
||||||
]
|
|
||||||
tlog.info(
|
tlog.info(
|
||||||
f"Loading {len(repos)} cached repositories: [{', '.join(repo_names)}]"
|
f"Loading {len(repos)} cached repositories: [{', '.join(repo_names)}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
return [GiteaProject(self.config, self.webhook_secret, repo) for repo in repos]
|
return [
|
||||||
|
GiteaProject(
|
||||||
|
self.config, self.config.webhook_secret, RepoData.model_validate(repo)
|
||||||
|
)
|
||||||
|
for repo in repos
|
||||||
|
]
|
||||||
|
|
||||||
def are_projects_cached(self) -> bool:
|
def are_projects_cached(self) -> bool:
|
||||||
return self.config.project_cache_file.exists()
|
return self.config.project_cache_file.exists()
|
||||||
|
@ -245,30 +240,24 @@ class CreateGiteaProjectHooks(ThreadDeferredBuildStep):
|
||||||
name = "create_gitea_project_hooks"
|
name = "create_gitea_project_hooks"
|
||||||
|
|
||||||
config: GiteaConfig
|
config: GiteaConfig
|
||||||
project_cache_file: Path
|
|
||||||
webhook_secret: str
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: GiteaConfig,
|
config: GiteaConfig,
|
||||||
project_cache_file: Path,
|
|
||||||
webhook_secret: str,
|
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.project_cache_file = project_cache_file
|
|
||||||
self.webhook_secret = webhook_secret
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def run_deferred(self) -> None:
|
def run_deferred(self) -> None:
|
||||||
repos = json.loads(self.project_cache_file.read_text())
|
repos = model_validate_project_cache(RepoData, self.config.project_cache_file)
|
||||||
|
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
create_repo_hook(
|
create_repo_hook(
|
||||||
self.config.token(),
|
self.config.token,
|
||||||
self.webhook_secret,
|
self.config.webhook_secret,
|
||||||
repo["owner"]["login"],
|
repo.owner.login,
|
||||||
repo["name"],
|
repo.name,
|
||||||
self.config.instance_url,
|
self.config.instance_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -294,24 +283,24 @@ class ReloadGiteaProjects(ThreadDeferredBuildStep):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def run_deferred(self) -> None:
|
def run_deferred(self) -> None:
|
||||||
repos = filter_repos_by_topic(
|
repos: list[RepoData] = filter_repos_by_topic(
|
||||||
self.config.topic,
|
self.config.topic,
|
||||||
refresh_projects(self.config, self.project_cache_file),
|
refresh_projects(self.config, self.project_cache_file),
|
||||||
lambda repo: repo["topics"],
|
lambda repo: repo.topics,
|
||||||
)
|
)
|
||||||
|
|
||||||
atomic_write_file(self.project_cache_file, json.dumps(repos))
|
atomic_write_file(self.project_cache_file, model_dump_project_cache(repos))
|
||||||
|
|
||||||
def run_post(self) -> Any:
|
def run_post(self) -> Any:
|
||||||
return util.SUCCESS
|
return util.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
def refresh_projects(config: GiteaConfig, repo_cache_file: Path) -> list[Any]:
|
def refresh_projects(config: GiteaConfig, repo_cache_file: Path) -> list[RepoData]:
|
||||||
repos = []
|
repos = []
|
||||||
|
|
||||||
for repo in paginated_github_request(
|
for repo in paginated_github_request(
|
||||||
f"{config.instance_url}/api/v1/user/repos?limit=100",
|
f"{config.instance_url}/api/v1/user/repos?limit=100",
|
||||||
config.token(),
|
config.token,
|
||||||
):
|
):
|
||||||
if not repo["permissions"]["admin"]:
|
if not repo["permissions"]["admin"]:
|
||||||
name = repo["full_name"]
|
name = repo["full_name"]
|
||||||
|
@ -323,10 +312,10 @@ def refresh_projects(config: GiteaConfig, repo_cache_file: Path) -> list[Any]:
|
||||||
# Gitea doesn't include topics in the default repo listing, unlike GitHub
|
# Gitea doesn't include topics in the default repo listing, unlike GitHub
|
||||||
topics: list[str] = http_request(
|
topics: list[str] = http_request(
|
||||||
f"{config.instance_url}/api/v1/repos/{repo['owner']['login']}/{repo['name']}/topics",
|
f"{config.instance_url}/api/v1/repos/{repo['owner']['login']}/{repo['name']}/topics",
|
||||||
headers={"Authorization": f"token {config.token()}"},
|
headers={"Authorization": f"token {config.token}"},
|
||||||
).json()["topics"]
|
).json()["topics"]
|
||||||
repo["topics"] = topics
|
repo["topics"] = topics
|
||||||
repos.append(repo)
|
repos.append(RepoData.model_validate(repo))
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AuthType:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AuthTypeLegacy(AuthType):
|
|
||||||
token_secret_name: str = "github-token"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AuthTypeApp(AuthType):
|
|
||||||
app_id: int
|
|
||||||
app_secret_key_name: str = "github-app-secret-key"
|
|
||||||
app_installation_token_map_name: Path = Path(
|
|
||||||
"github-app-installation-token-map.json"
|
|
||||||
)
|
|
||||||
app_project_id_map_name: Path = Path("github-app-project-id-map-name.json")
|
|
||||||
app_jwt_token_name: Path = Path("github-app-jwt-token")
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .repo_token import RepoToken
|
from .repo_token import RepoToken
|
||||||
|
@ -10,7 +11,7 @@ from .repo_token import RepoToken
|
||||||
|
|
||||||
class JWTToken(RepoToken):
|
class JWTToken(RepoToken):
|
||||||
app_id: int
|
app_id: int
|
||||||
app_private_key: str
|
app_private_key_file: Path
|
||||||
lifetime: timedelta
|
lifetime: timedelta
|
||||||
|
|
||||||
expiration: datetime
|
expiration: datetime
|
||||||
|
@ -19,20 +20,20 @@ class JWTToken(RepoToken):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
app_id: int,
|
app_id: int,
|
||||||
app_private_key: str,
|
app_private_key_file: Path,
|
||||||
lifetime: timedelta = timedelta(minutes=10),
|
lifetime: timedelta = timedelta(minutes=10),
|
||||||
) -> None:
|
) -> None:
|
||||||
self.app_id = app_id
|
self.app_id = app_id
|
||||||
self.app_private_key = app_private_key
|
self.app_private_key_file = app_private_key_file
|
||||||
self.lifetime = lifetime
|
self.lifetime = lifetime
|
||||||
|
|
||||||
self.token, self.expiration = JWTToken.generate_token(
|
self.token, self.expiration = JWTToken.generate_token(
|
||||||
self.app_id, self.app_private_key, lifetime
|
self.app_id, self.app_private_key_file, lifetime
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_token(
|
def generate_token(
|
||||||
app_id: int, app_private_key: str, lifetime: timedelta
|
app_id: int, app_private_key_file: Path, lifetime: timedelta
|
||||||
) -> tuple[str, datetime]:
|
) -> tuple[str, datetime]:
|
||||||
def build_jwt_payload(
|
def build_jwt_payload(
|
||||||
app_id: int, lifetime: timedelta
|
app_id: int, lifetime: timedelta
|
||||||
|
@ -48,9 +49,9 @@ class JWTToken(RepoToken):
|
||||||
}
|
}
|
||||||
return (jwt_payload, exp)
|
return (jwt_payload, exp)
|
||||||
|
|
||||||
def rs256_sign(data: str, private_key: str) -> str:
|
def rs256_sign(data: str, private_key_file: Path) -> str:
|
||||||
signature = subprocess.run(
|
signature = subprocess.run(
|
||||||
["openssl", "dgst", "-binary", "-sha256", "-sign", private_key],
|
["openssl", "dgst", "-binary", "-sha256", "-sign", private_key_file],
|
||||||
input=data.encode("utf-8"),
|
input=data.encode("utf-8"),
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
check=True,
|
check=True,
|
||||||
|
@ -65,7 +66,7 @@ class JWTToken(RepoToken):
|
||||||
jwt_payload = json.dumps(jwt).encode("utf-8")
|
jwt_payload = json.dumps(jwt).encode("utf-8")
|
||||||
json_headers = json.dumps({"alg": "RS256", "typ": "JWT"}).encode("utf-8")
|
json_headers = json.dumps({"alg": "RS256", "typ": "JWT"}).encode("utf-8")
|
||||||
encoded_jwt_parts = f"{base64url(json_headers)}.{base64url(jwt_payload)}"
|
encoded_jwt_parts = f"{base64url(json_headers)}.{base64url(jwt_payload)}"
|
||||||
encoded_mac = rs256_sign(encoded_jwt_parts, app_private_key)
|
encoded_mac = rs256_sign(encoded_jwt_parts, app_private_key_file)
|
||||||
return (f"{encoded_jwt_parts}.{encoded_mac}", expiration)
|
return (f"{encoded_jwt_parts}.{encoded_mac}", expiration)
|
||||||
|
|
||||||
# installations = paginated_github_request("https://api.github.com/app/installations?per_page=100", generated_jwt)
|
# installations = paginated_github_request("https://api.github.com/app/installations?per_page=100", generated_jwt)
|
||||||
|
@ -75,7 +76,7 @@ class JWTToken(RepoToken):
|
||||||
def get(self) -> str:
|
def get(self) -> str:
|
||||||
if self.expiration - datetime.now(tz=UTC) < self.lifetime * 0.2:
|
if self.expiration - datetime.now(tz=UTC) < self.lifetime * 0.2:
|
||||||
self.token, self.expiration = JWTToken.generate_token(
|
self.token, self.expiration = JWTToken.generate_token(
|
||||||
self.app_id, self.app_private_key, self.lifetime
|
self.app_id, self.app_private_key_file, self.lifetime
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.token
|
return self.token
|
||||||
|
|
|
@ -17,6 +17,7 @@ from buildbot.secrets.providers.base import SecretProviderBase
|
||||||
from buildbot.www.auth import AuthBase
|
from buildbot.www.auth import AuthBase
|
||||||
from buildbot.www.avatar import AvatarBase, AvatarGitHub
|
from buildbot.www.avatar import AvatarBase, AvatarGitHub
|
||||||
from buildbot.www.oauth2 import GitHubAuth
|
from buildbot.www.oauth2 import GitHubAuth
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from twisted.logger import Logger
|
from twisted.logger import Logger
|
||||||
from twisted.python import log
|
from twisted.python import log
|
||||||
|
|
||||||
|
@ -25,10 +26,11 @@ from .common import (
|
||||||
atomic_write_file,
|
atomic_write_file,
|
||||||
filter_repos_by_topic,
|
filter_repos_by_topic,
|
||||||
http_request,
|
http_request,
|
||||||
|
model_dump_project_cache,
|
||||||
|
model_validate_project_cache,
|
||||||
paginated_github_request,
|
paginated_github_request,
|
||||||
slugify_project_name,
|
slugify_project_name,
|
||||||
)
|
)
|
||||||
from .github.auth._type import AuthType, AuthTypeApp, AuthTypeLegacy
|
|
||||||
from .github.installation_token import InstallationToken
|
from .github.installation_token import InstallationToken
|
||||||
from .github.jwt_token import JWTToken
|
from .github.jwt_token import JWTToken
|
||||||
from .github.legacy_token import (
|
from .github.legacy_token import (
|
||||||
|
@ -37,12 +39,33 @@ from .github.legacy_token import (
|
||||||
from .github.repo_token import (
|
from .github.repo_token import (
|
||||||
RepoToken,
|
RepoToken,
|
||||||
)
|
)
|
||||||
|
from .models import (
|
||||||
|
GitHubAppConfig,
|
||||||
|
GitHubConfig,
|
||||||
|
GitHubLegacyConfig,
|
||||||
|
)
|
||||||
from .projects import GitBackend, GitProject
|
from .projects import GitBackend, GitProject
|
||||||
from .secrets import read_secret_file
|
|
||||||
|
|
||||||
tlog = Logger()
|
tlog = Logger()
|
||||||
|
|
||||||
|
|
||||||
|
class RepoOwnerData(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
login: str
|
||||||
|
ttype: str = Field(alias="type")
|
||||||
|
|
||||||
|
|
||||||
|
class RepoData(BaseModel):
|
||||||
|
name: str
|
||||||
|
owner: RepoOwnerData
|
||||||
|
full_name: str
|
||||||
|
html_url: str
|
||||||
|
default_branch: str
|
||||||
|
topics: list[str]
|
||||||
|
installation_id: int | None
|
||||||
|
|
||||||
|
|
||||||
def get_installations(jwt_token: JWTToken) -> list[int]:
|
def get_installations(jwt_token: JWTToken) -> list[int]:
|
||||||
installations = paginated_github_request(
|
installations = paginated_github_request(
|
||||||
"https://api.github.com/app/installations?per_page=100", jwt_token.get()
|
"https://api.github.com/app/installations?per_page=100", jwt_token.get()
|
||||||
|
@ -80,7 +103,7 @@ class CreateGitHubInstallationHooks(ThreadDeferredBuildStep):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def run_deferred(self) -> None:
|
def run_deferred(self) -> None:
|
||||||
repos = json.loads(self.project_cache_file.read_text())
|
repos = model_validate_project_cache(RepoData, self.project_cache_file)
|
||||||
installation_token_map: dict[int, InstallationToken] = dict(
|
installation_token_map: dict[int, InstallationToken] = dict(
|
||||||
starmap(
|
starmap(
|
||||||
lambda k, v: (
|
lambda k, v: (
|
||||||
|
@ -94,11 +117,14 @@ class CreateGitHubInstallationHooks(ThreadDeferredBuildStep):
|
||||||
)
|
)
|
||||||
|
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
|
if repo.installation_id is None:
|
||||||
|
continue
|
||||||
|
|
||||||
create_project_hook(
|
create_project_hook(
|
||||||
installation_token_map[repo["installation_id"]],
|
installation_token_map[repo.installation_id],
|
||||||
self.webhook_secret,
|
self.webhook_secret,
|
||||||
repo["owner"]["login"],
|
repo.owner.login,
|
||||||
repo["name"],
|
repo.name,
|
||||||
self.webhook_url,
|
self.webhook_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -144,7 +170,7 @@ class ReloadGithubInstallations(ThreadDeferredBuildStep):
|
||||||
get_installations(self.jwt_token),
|
get_installations(self.jwt_token),
|
||||||
)
|
)
|
||||||
|
|
||||||
repos: list[Any] = []
|
repos: list[RepoData] = []
|
||||||
project_id_map: dict[str, int] = {}
|
project_id_map: dict[str, int] = {}
|
||||||
|
|
||||||
repos = []
|
repos = []
|
||||||
|
@ -160,17 +186,17 @@ class ReloadGithubInstallations(ThreadDeferredBuildStep):
|
||||||
subkey="repositories",
|
subkey="repositories",
|
||||||
require_admin=False,
|
require_admin=False,
|
||||||
),
|
),
|
||||||
lambda repo: repo["topics"],
|
lambda repo: repo.topics,
|
||||||
)
|
)
|
||||||
|
|
||||||
for repo in new_repos:
|
for repo in new_repos:
|
||||||
repo["installation_id"] = k
|
repo.installation_id = k
|
||||||
|
|
||||||
project_id_map[repo["full_name"]] = k
|
project_id_map[repo.full_name] = k
|
||||||
|
|
||||||
repos.extend(new_repos)
|
repos.extend(new_repos)
|
||||||
|
|
||||||
atomic_write_file(self.project_cache_file, json.dumps(repos))
|
atomic_write_file(self.project_cache_file, model_dump_project_cache(repos))
|
||||||
atomic_write_file(self.project_id_map_name, json.dumps(project_id_map))
|
atomic_write_file(self.project_id_map_name, json.dumps(project_id_map))
|
||||||
|
|
||||||
tlog.info(
|
tlog.info(
|
||||||
|
@ -207,14 +233,14 @@ class CreateGitHubProjectHooks(ThreadDeferredBuildStep):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def run_deferred(self) -> None:
|
def run_deferred(self) -> None:
|
||||||
repos = json.loads(self.project_cache_file.read_text())
|
repos = model_validate_project_cache(RepoData, self.project_cache_file)
|
||||||
|
|
||||||
for repo in repos:
|
for repo in repos:
|
||||||
create_project_hook(
|
create_project_hook(
|
||||||
self.token,
|
self.token,
|
||||||
self.webhook_secret,
|
self.webhook_secret,
|
||||||
repo["owner"]["login"],
|
repo.owner.login,
|
||||||
repo["name"],
|
repo.name,
|
||||||
self.webhook_url,
|
self.webhook_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -244,13 +270,13 @@ class ReloadGithubProjects(ThreadDeferredBuildStep):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def run_deferred(self) -> None:
|
def run_deferred(self) -> None:
|
||||||
repos: list[Any] = filter_repos_by_topic(
|
repos: list[RepoData] = filter_repos_by_topic(
|
||||||
self.topic,
|
self.topic,
|
||||||
refresh_projects(self.token.get(), self.project_cache_file),
|
refresh_projects(self.token.get(), self.project_cache_file),
|
||||||
lambda repo: repo["topics"],
|
lambda repo: repo.topics,
|
||||||
)
|
)
|
||||||
|
|
||||||
atomic_write_file(self.project_cache_file, json.dumps(repos))
|
atomic_write_file(self.project_cache_file, model_dump_project_cache(repos))
|
||||||
|
|
||||||
def run_post(self) -> Any:
|
def run_post(self) -> Any:
|
||||||
return util.SUCCESS
|
return util.SUCCESS
|
||||||
|
@ -262,7 +288,7 @@ class GithubAuthBackend(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_repo_token(self, repo: dict[str, Any]) -> RepoToken:
|
def get_repo_token(self, repo: RepoData) -> RepoToken:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -285,18 +311,18 @@ class GithubAuthBackend(ABC):
|
||||||
|
|
||||||
|
|
||||||
class GithubLegacyAuthBackend(GithubAuthBackend):
|
class GithubLegacyAuthBackend(GithubAuthBackend):
|
||||||
auth_type: AuthTypeLegacy
|
auth_type: GitHubLegacyConfig
|
||||||
|
|
||||||
token: LegacyToken
|
token: LegacyToken
|
||||||
|
|
||||||
def __init__(self, auth_type: AuthTypeLegacy) -> None:
|
def __init__(self, auth_type: GitHubLegacyConfig) -> None:
|
||||||
self.auth_type = auth_type
|
self.auth_type = auth_type
|
||||||
self.token = LegacyToken(read_secret_file(auth_type.token_secret_name))
|
self.token = LegacyToken(auth_type.token)
|
||||||
|
|
||||||
def get_general_token(self) -> RepoToken:
|
def get_general_token(self) -> RepoToken:
|
||||||
return self.token
|
return self.token
|
||||||
|
|
||||||
def get_repo_token(self, repo: dict[str, Any]) -> RepoToken:
|
def get_repo_token(self, repo: RepoData) -> RepoToken:
|
||||||
return self.token
|
return self.token
|
||||||
|
|
||||||
def create_secret_providers(self) -> list[SecretProviderBase]:
|
def create_secret_providers(self) -> list[SecretProviderBase]:
|
||||||
|
@ -351,24 +377,22 @@ class GitHubLegacySecretService(SecretProviderBase):
|
||||||
|
|
||||||
|
|
||||||
class GithubAppAuthBackend(GithubAuthBackend):
|
class GithubAppAuthBackend(GithubAuthBackend):
|
||||||
auth_type: AuthTypeApp
|
auth_type: GitHubAppConfig
|
||||||
|
|
||||||
jwt_token: JWTToken
|
jwt_token: JWTToken
|
||||||
installation_tokens: dict[int, InstallationToken]
|
installation_tokens: dict[int, InstallationToken]
|
||||||
project_id_map: dict[str, int]
|
project_id_map: dict[str, int]
|
||||||
|
|
||||||
def __init__(self, auth_type: AuthTypeApp) -> None:
|
def __init__(self, auth_type: GitHubAppConfig) -> None:
|
||||||
self.auth_type = auth_type
|
self.auth_type = auth_type
|
||||||
self.jwt_token = JWTToken(
|
self.jwt_token = JWTToken(self.auth_type.id, self.auth_type.secret_key_file)
|
||||||
self.auth_type.app_id, self.auth_type.app_secret_key_name
|
|
||||||
)
|
|
||||||
self.installation_tokens = GithubBackend.load_installations(
|
self.installation_tokens = GithubBackend.load_installations(
|
||||||
self.jwt_token,
|
self.jwt_token,
|
||||||
self.auth_type.app_installation_token_map_name,
|
self.auth_type.installation_token_map_file,
|
||||||
)
|
)
|
||||||
if self.auth_type.app_project_id_map_name.exists():
|
if self.auth_type.project_id_map_file.exists():
|
||||||
self.project_id_map = json.loads(
|
self.project_id_map = json.loads(
|
||||||
self.auth_type.app_project_id_map_name.read_text()
|
self.auth_type.project_id_map_file.read_text()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tlog.info(
|
tlog.info(
|
||||||
|
@ -379,9 +403,9 @@ class GithubAppAuthBackend(GithubAuthBackend):
|
||||||
def get_general_token(self) -> RepoToken:
|
def get_general_token(self) -> RepoToken:
|
||||||
return self.jwt_token
|
return self.jwt_token
|
||||||
|
|
||||||
def get_repo_token(self, repo: dict[str, Any]) -> RepoToken:
|
def get_repo_token(self, repo: RepoData) -> RepoToken:
|
||||||
assert "installation_id" in repo, f"Missing installation_id in {repo}"
|
assert repo.installation_id is not None, f"Missing installation_id in {repo}"
|
||||||
return self.installation_tokens[repo["installation_id"]]
|
return self.installation_tokens[repo.installation_id]
|
||||||
|
|
||||||
def create_secret_providers(self) -> list[SecretProviderBase]:
|
def create_secret_providers(self) -> list[SecretProviderBase]:
|
||||||
return [GitHubAppSecretService(self.installation_tokens, self.jwt_token)]
|
return [GitHubAppSecretService(self.installation_tokens, self.jwt_token)]
|
||||||
|
@ -411,14 +435,14 @@ class GithubAppAuthBackend(GithubAuthBackend):
|
||||||
ReloadGithubInstallations(
|
ReloadGithubInstallations(
|
||||||
self.jwt_token,
|
self.jwt_token,
|
||||||
project_cache_file,
|
project_cache_file,
|
||||||
self.auth_type.app_installation_token_map_name,
|
self.auth_type.installation_token_map_file,
|
||||||
self.auth_type.app_project_id_map_name,
|
self.auth_type.project_id_map_file,
|
||||||
topic,
|
topic,
|
||||||
),
|
),
|
||||||
CreateGitHubInstallationHooks(
|
CreateGitHubInstallationHooks(
|
||||||
self.jwt_token,
|
self.jwt_token,
|
||||||
project_cache_file,
|
project_cache_file,
|
||||||
self.auth_type.app_installation_token_map_name,
|
self.auth_type.installation_token_map_file,
|
||||||
webhook_secret=webhook_secret,
|
webhook_secret=webhook_secret,
|
||||||
webhook_url=webhook_url,
|
webhook_url=webhook_url,
|
||||||
topic=topic,
|
topic=topic,
|
||||||
|
@ -450,32 +474,22 @@ class GitHubAppSecretService(SecretProviderBase):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GithubConfig:
|
|
||||||
oauth_id: str | None
|
|
||||||
auth_type: AuthType
|
|
||||||
oauth_secret_name: str = "github-oauth-secret"
|
|
||||||
webhook_secret_name: str = "github-webhook-secret"
|
|
||||||
project_cache_file: Path = Path("github-project-cache-v1.json")
|
|
||||||
topic: str | None = "build-with-buildbot"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GithubBackend(GitBackend):
|
class GithubBackend(GitBackend):
|
||||||
config: GithubConfig
|
config: GitHubConfig
|
||||||
webhook_secret: str
|
webhook_secret: str
|
||||||
webhook_url: str
|
webhook_url: str
|
||||||
|
|
||||||
auth_backend: GithubAuthBackend
|
auth_backend: GithubAuthBackend
|
||||||
|
|
||||||
def __init__(self, config: GithubConfig, webhook_url: str) -> None:
|
def __init__(self, config: GitHubConfig, webhook_url: str) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.webhook_secret = read_secret_file(self.config.webhook_secret_name)
|
self.webhook_secret = self.config.webhook_secret
|
||||||
self.webhook_url = webhook_url
|
self.webhook_url = webhook_url
|
||||||
|
|
||||||
if isinstance(self.config.auth_type, AuthTypeLegacy):
|
if isinstance(self.config.auth_type, GitHubLegacyConfig):
|
||||||
self.auth_backend = GithubLegacyAuthBackend(self.config.auth_type)
|
self.auth_backend = GithubLegacyAuthBackend(self.config.auth_type)
|
||||||
elif isinstance(self.config.auth_type, AuthTypeApp):
|
elif isinstance(self.config.auth_type, GitHubAppConfig):
|
||||||
self.auth_backend = GithubAppAuthBackend(self.config.auth_type)
|
self.auth_backend = GithubAppAuthBackend(self.config.auth_type)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -551,7 +565,7 @@ class GithubBackend(GitBackend):
|
||||||
assert self.config.oauth_id is not None, "GitHub OAuth ID is required"
|
assert self.config.oauth_id is not None, "GitHub OAuth ID is required"
|
||||||
return GitHubAuth(
|
return GitHubAuth(
|
||||||
self.config.oauth_id,
|
self.config.oauth_id,
|
||||||
read_secret_file(self.config.oauth_secret_name),
|
self.config.oauth_secret,
|
||||||
apiVersion=4,
|
apiVersion=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -562,30 +576,28 @@ class GithubBackend(GitBackend):
|
||||||
if not self.config.project_cache_file.exists():
|
if not self.config.project_cache_file.exists():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
repos: list[dict[str, Any]] = filter_repos_by_topic(
|
repos: list[RepoData] = filter_repos_by_topic(
|
||||||
self.config.topic,
|
self.config.topic,
|
||||||
sorted(
|
sorted(
|
||||||
json.loads(self.config.project_cache_file.read_text()),
|
model_validate_project_cache(RepoData, self.config.project_cache_file),
|
||||||
key=lambda x: x["full_name"],
|
key=lambda repo: repo.full_name,
|
||||||
),
|
),
|
||||||
lambda repo: repo["topics"],
|
lambda repo: repo.topics,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(self.auth_backend, GithubAppAuthBackend):
|
if isinstance(self.auth_backend, GithubAppAuthBackend):
|
||||||
dropped_repos = list(
|
dropped_repos = list(
|
||||||
filter(lambda repo: "installation_id" not in repo, repos)
|
filter(lambda repo: repo.installation_id is None, repos)
|
||||||
)
|
)
|
||||||
if dropped_repos:
|
if dropped_repos:
|
||||||
tlog.info(
|
tlog.info(
|
||||||
"Dropped projects follow, refresh will follow after initialisation:"
|
"Dropped projects follow, refresh will follow after initialisation:"
|
||||||
)
|
)
|
||||||
for dropped_repo in dropped_repos:
|
for dropped_repo in dropped_repos:
|
||||||
tlog.info(f"\tDropping repo {dropped_repo['full_name']}")
|
tlog.info(f"\tDropping repo {dropped_repo.full_name}")
|
||||||
repos = list(filter(lambda repo: "installation_id" in repo, repos))
|
repos = list(filter(lambda repo: repo.installation_id is not None, repos))
|
||||||
|
|
||||||
repo_names: list[str] = [
|
repo_names: list[str] = [repo.owner.login + "/" + repo.name for repo in repos]
|
||||||
repo["owner"]["login"] + "/" + repo["name"] for repo in repos
|
|
||||||
]
|
|
||||||
|
|
||||||
tlog.info(
|
tlog.info(
|
||||||
f"Loading {len(repos)} cached repositories: [{', '.join(repo_names)}]"
|
f"Loading {len(repos)} cached repositories: [{', '.join(repo_names)}]"
|
||||||
|
@ -595,7 +607,7 @@ class GithubBackend(GitBackend):
|
||||||
self.auth_backend.get_repo_token(repo),
|
self.auth_backend.get_repo_token(repo),
|
||||||
self.config,
|
self.config,
|
||||||
self.webhook_secret,
|
self.webhook_secret,
|
||||||
repo,
|
RepoData.model_validate(repo),
|
||||||
)
|
)
|
||||||
for repo in repos
|
for repo in repos
|
||||||
]
|
]
|
||||||
|
@ -605,14 +617,16 @@ class GithubBackend(GitBackend):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isinstance(self.config.auth_type, AuthTypeApp)
|
isinstance(self.config.auth_type, GitHubAppConfig)
|
||||||
and not self.config.auth_type.app_project_id_map_name.exists()
|
and not self.config.auth_type.project_id_map_file.exists()
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
all_have_installation_id = True
|
all_have_installation_id = True
|
||||||
for project in json.loads(self.config.project_cache_file.read_text()):
|
for project in model_validate_project_cache(
|
||||||
if "installation_id" not in project:
|
RepoData, self.config.project_cache_file
|
||||||
|
):
|
||||||
|
if project.installation_id is not None:
|
||||||
all_have_installation_id = False
|
all_have_installation_id = False
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -669,17 +683,17 @@ def create_project_hook(
|
||||||
|
|
||||||
|
|
||||||
class GithubProject(GitProject):
|
class GithubProject(GitProject):
|
||||||
config: GithubConfig
|
config: GitHubConfig
|
||||||
webhook_secret: str
|
webhook_secret: str
|
||||||
data: dict[str, Any]
|
data: RepoData
|
||||||
token: RepoToken
|
token: RepoToken
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
token: RepoToken,
|
token: RepoToken,
|
||||||
config: GithubConfig,
|
config: GitHubConfig,
|
||||||
webhook_secret: str,
|
webhook_secret: str,
|
||||||
data: dict[str, Any],
|
data: RepoData,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.token = token
|
self.token = token
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -699,35 +713,35 @@ class GithubProject(GitProject):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repo(self) -> str:
|
def repo(self) -> str:
|
||||||
return self.data["name"]
|
return self.data.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def owner(self) -> str:
|
def owner(self) -> str:
|
||||||
return self.data["owner"]["login"]
|
return self.data.owner.login
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self.data["full_name"]
|
return self.data.full_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> str:
|
def url(self) -> str:
|
||||||
return self.data["html_url"]
|
return self.data.html_url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project_id(self) -> str:
|
def project_id(self) -> str:
|
||||||
return slugify_project_name(self.data["full_name"])
|
return slugify_project_name(self.data.full_name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_branch(self) -> str:
|
def default_branch(self) -> str:
|
||||||
return self.data["default_branch"]
|
return self.data.default_branch
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def topics(self) -> list[str]:
|
def topics(self) -> list[str]:
|
||||||
return self.data["topics"]
|
return self.data.topics
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def belongs_to_org(self) -> bool:
|
def belongs_to_org(self) -> bool:
|
||||||
return self.data["owner"]["type"] == "Organization"
|
return self.data.owner.ttype == "Organization"
|
||||||
|
|
||||||
|
|
||||||
def refresh_projects(
|
def refresh_projects(
|
||||||
|
@ -738,7 +752,7 @@ def refresh_projects(
|
||||||
api_endpoint: str = "/user/repos",
|
api_endpoint: str = "/user/repos",
|
||||||
subkey: None | str = None,
|
subkey: None | str = None,
|
||||||
require_admin: bool = True,
|
require_admin: bool = True,
|
||||||
) -> list[Any]:
|
) -> list[RepoData]:
|
||||||
if repos is None:
|
if repos is None:
|
||||||
repos = []
|
repos = []
|
||||||
|
|
||||||
|
@ -754,6 +768,7 @@ def refresh_projects(
|
||||||
f"skipping {name} because we do not have admin privileges, needed for hook management",
|
f"skipping {name} because we do not have admin privileges, needed for hook management",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
repos.append(repo)
|
repo["installation_id"] = None
|
||||||
|
repos.append(RepoData.model_validate(repo))
|
||||||
|
|
||||||
return repos
|
return repos
|
||||||
|
|
182
buildbot_nix/models.py
Normal file
182
buildbot_nix/models.py
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, field_serializer, field_validator, ConfigDict
|
||||||
|
from buildbot.plugins import util, steps
|
||||||
|
|
||||||
|
from .secrets import read_secret_file
|
||||||
|
|
||||||
|
class InternalIssue(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def exclude_fields(fields: list[str]) -> dict[str, dict[str, bool]]:
|
||||||
|
return dict(map(lambda k: (k, {"exclude": True}), fields))
|
||||||
|
|
||||||
|
class AuthBackendConfig(str, Enum):
|
||||||
|
github = "github"
|
||||||
|
gitea = "gitea"
|
||||||
|
none = "none"
|
||||||
|
|
||||||
|
class CachixConfig(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
signing_key_file: Path | None
|
||||||
|
auth_token_file: Path | None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signing_key(self) -> str:
|
||||||
|
if self.signing_key_file is None:
|
||||||
|
raise InternalIssue
|
||||||
|
return read_secret_file(self.signing_key_file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def auth_token(self) -> str:
|
||||||
|
if self.auth_token_file is None:
|
||||||
|
raise InternalIssue
|
||||||
|
return read_secret_file(self.auth_token_file)
|
||||||
|
|
||||||
|
# TODO why did the original implementation return an empty env if both files were missing?
|
||||||
|
@property
|
||||||
|
def environment(self) -> dict[str, str]:
|
||||||
|
environment = {}
|
||||||
|
environment["CACHIX_SIGNING_KEY"] = util.Secret(self.signing_key_file)
|
||||||
|
environment["CACHIX_AUTH_TOKEN"] = util.Secret(self.auth_token_file)
|
||||||
|
return environment
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = exclude_fields(["singing_key", "auth_token"])
|
||||||
|
|
||||||
|
class GiteaConfig(BaseModel):
|
||||||
|
instance_url: str
|
||||||
|
topic: str | None
|
||||||
|
|
||||||
|
token_file: Path = Field(default = Path("gitea-token"))
|
||||||
|
webhook_secret_file: Path = Field(default = Path("gitea-webhook-secret"))
|
||||||
|
project_cache_file: Path = Field(default = Path("gitea-project-cache.json"))
|
||||||
|
|
||||||
|
oauth_id: str | None
|
||||||
|
oauth_secret_file: Path | None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self) -> str:
|
||||||
|
return read_secret_file(self.token_file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def webhook_secret(self) -> str:
|
||||||
|
return read_secret_file(self.webhook_secret_file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oauth_secret(self) -> str:
|
||||||
|
if self.oauth_secret_file is None:
|
||||||
|
raise InternalIssue
|
||||||
|
return read_secret_file(self.oauth_secret_file)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = exclude_fields(["token", "webhook_secret", "oauth_secret"])
|
||||||
|
|
||||||
|
class GitHubLegacyConfig(BaseModel):
|
||||||
|
token_file: Path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self) -> str:
|
||||||
|
return read_secret_file(self.token_file)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = exclude_fields(["token"])
|
||||||
|
|
||||||
|
class GitHubAppConfig(BaseModel):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
secret_key_file: Path
|
||||||
|
installation_token_map_file: Path = Field(default = Path(
|
||||||
|
"github-app-installation-token-map.json"
|
||||||
|
))
|
||||||
|
project_id_map_file: Path = Field(default = Path(
|
||||||
|
"github-app-project-id-map-name.json"
|
||||||
|
))
|
||||||
|
jwt_token_map: Path = Field(default = Path(
|
||||||
|
"github-app-jwt-token"
|
||||||
|
))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret_key(self) -> str:
|
||||||
|
return read_secret_file(self.secret_key_file)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
fields = exclude_fields(["secret_key"])
|
||||||
|
|
||||||
|
class GitHubConfig(BaseModel):
|
||||||
|
auth_type: GitHubLegacyConfig | GitHubAppConfig
|
||||||
|
topic: str | None
|
||||||
|
|
||||||
|
project_cache_file: Path = Field(default = Path("github-project-cache-v1.json"))
|
||||||
|
webhook_secret_file: Path = Field(default = Path("github-webhook-secret"))
|
||||||
|
|
||||||
|
oauth_id: str | None
|
||||||
|
oauth_secret_file: Path | None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def webhook_secret(self) -> str:
|
||||||
|
return read_secret_file(self.webhook_secret_file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oauth_secret(self) -> str:
|
||||||
|
if self.oauth_secret_file is None:
|
||||||
|
raise InternalIssue
|
||||||
|
return read_secret_file(self.oauth_secret_file)
|
||||||
|
|
||||||
|
# note that serialization isn't correct, as there is no way to *rename* the field `nix_type` to `_type`,
|
||||||
|
# one must always specify `by_alias = True`, such as `model_dump(by_alias = True)`, relevant issue:
|
||||||
|
# https://github.com/pydantic/pydantic/issues/8379
|
||||||
|
class Interpolate(BaseModel):
|
||||||
|
model_config = ConfigDict(populate_by_name=True)
|
||||||
|
|
||||||
|
nix_type: str = Field(alias = "_type")
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class PostBuildStep(BaseModel):
|
||||||
|
name: str
|
||||||
|
environment: dict[str, str | Interpolate]
|
||||||
|
command: list[str | Interpolate]
|
||||||
|
|
||||||
|
def to_buildstep(self) -> steps.BuildStep:
|
||||||
|
def maybe_interpolate(value: str | Interpolate) -> str | util.Interpolate:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
return util.Interpolate(value.value)
|
||||||
|
|
||||||
|
return steps.ShellCommand(
|
||||||
|
name = self.name,
|
||||||
|
env = {
|
||||||
|
k: maybe_interpolate(k) for k in self.environment
|
||||||
|
},
|
||||||
|
command = [
|
||||||
|
maybe_interpolate(x) for x in self.command
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
class BuildbotNixConfig(BaseModel):
|
||||||
|
db_url: str
|
||||||
|
auth_backend: AuthBackendConfig
|
||||||
|
build_retries: int
|
||||||
|
cachix: CachixConfig | None
|
||||||
|
gitea: GiteaConfig | None
|
||||||
|
github: GitHubConfig | None
|
||||||
|
admins: list[str]
|
||||||
|
workers_file: Path
|
||||||
|
build_systems: list[str]
|
||||||
|
eval_max_memory_size: int
|
||||||
|
eval_worker_count: int | None
|
||||||
|
nix_workers_secret_file: Path = Field(default = Path("buildbot-nix-workers"))
|
||||||
|
domain: str
|
||||||
|
webhook_base_url: str
|
||||||
|
use_https: bool
|
||||||
|
outputs_path: Path | None
|
||||||
|
url: str
|
||||||
|
post_build_steps: list[PostBuildStep]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nix_workers_secret(self) -> str:
|
||||||
|
return read_secret_file(self.nix_workers_secret_file)
|
|
@ -3,9 +3,9 @@ import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def read_secret_file(secret_name: str) -> str:
|
def read_secret_file(secret_file: Path) -> str:
|
||||||
directory = os.environ.get("CREDENTIALS_DIRECTORY")
|
directory = os.environ.get("CREDENTIALS_DIRECTORY")
|
||||||
if directory is None:
|
if directory is None:
|
||||||
print("directory not set", file=sys.stderr)
|
print("directory not set", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return Path(directory).joinpath(secret_name).read_text().rstrip()
|
return Path(directory).joinpath(secret_file).read_text().rstrip()
|
||||||
|
|
201
nix/master.nix
201
nix/master.nix
|
@ -154,21 +154,20 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
cachix = {
|
cachix = {
|
||||||
|
enable = lib.mkEnableOption "Enable Cachix integration";
|
||||||
|
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.str;
|
||||||
default = null;
|
|
||||||
description = "Cachix name";
|
description = "Cachix name";
|
||||||
};
|
};
|
||||||
|
|
||||||
signingKeyFile = lib.mkOption {
|
signingKeyFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.path;
|
type = lib.types.path;
|
||||||
default = null;
|
|
||||||
description = "Cachix signing key";
|
description = "Cachix signing key";
|
||||||
};
|
};
|
||||||
|
|
||||||
authTokenFile = lib.mkOption {
|
authTokenFile = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.str;
|
||||||
default = null;
|
|
||||||
description = "Cachix auth token";
|
description = "Cachix auth token";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -291,8 +290,8 @@ in
|
||||||
description = "Systems that we will be build";
|
description = "Systems that we will be build";
|
||||||
};
|
};
|
||||||
evalMaxMemorySize = lib.mkOption {
|
evalMaxMemorySize = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.int;
|
||||||
default = "2048";
|
default = 2048;
|
||||||
description = ''
|
description = ''
|
||||||
Maximum memory size for nix-eval-jobs (in MiB) per
|
Maximum memory size for nix-eval-jobs (in MiB) per
|
||||||
worker. After the limit is reached, the worker is
|
worker. After the limit is reached, the worker is
|
||||||
|
@ -364,19 +363,28 @@ in
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
cfg.cachix.name != null -> cfg.cachix.signingKeyFile != null || cfg.cachix.authTokenFile != null;
|
cfg.authBackend == "github" -> (cfg.github.oauthId != null && cfg.github.oauthSecretFile != null);
|
||||||
message = "if cachix.name is provided, then cachix.signingKeyFile and cachix.authTokenFile must be set";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion =
|
|
||||||
cfg.authBackend != "github" || (cfg.github.oauthId != null && cfg.github.oauthSecretFile != null);
|
|
||||||
message = ''If config.services.buildbot-nix.master.authBackend is set to "github", then config.services.buildbot-nix.master.github.oauthId and config.services.buildbot-nix.master.github.oauthSecretFile have to be set.'';
|
message = ''If config.services.buildbot-nix.master.authBackend is set to "github", then config.services.buildbot-nix.master.github.oauthId and config.services.buildbot-nix.master.github.oauthSecretFile have to be set.'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
assertion =
|
assertion =
|
||||||
cfg.authBackend != "gitea" || (cfg.gitea.oauthId != null && cfg.gitea.oauthSecretFile != null);
|
cfg.authBackend == "gitea" -> (cfg.gitea.oauthId != null && cfg.gitea.oauthSecretFile != null);
|
||||||
message = ''config.services.buildbot-nix.master.authBackend is set to "gitea", then config.services.buildbot-nix.master.gitea.oauthId and config.services.buildbot-nix.master.gitea.oauthSecretFile have to be set.'';
|
message = ''config.services.buildbot-nix.master.authBackend is set to "gitea", then config.services.buildbot-nix.master.gitea.oauthId and config.services.buildbot-nix.master.gitea.oauthSecretFile have to be set.'';
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
cfg.authBackend == "github" -> cfg.github.enable;
|
||||||
|
message = ''
|
||||||
|
If `cfg.authBackend` is set to `"github"` the GitHub backend must be enabled with `cfg.github.enable`;
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
cfg.authBackend == "gitea" -> cfg.gitea.enable;
|
||||||
|
message = ''
|
||||||
|
If `cfg.authBackend` is set to `"gitea"` the GitHub backend must be enabled with `cfg.gitea.enable`;
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
services.buildbot-master = {
|
services.buildbot-master = {
|
||||||
|
@ -391,19 +399,11 @@ in
|
||||||
extraImports = ''
|
extraImports = ''
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from buildbot_nix import (
|
from buildbot_nix import (
|
||||||
GithubConfig,
|
|
||||||
NixConfigurator,
|
NixConfigurator,
|
||||||
CachixConfig,
|
BuildbotNixConfig,
|
||||||
GiteaConfig,
|
|
||||||
)
|
|
||||||
from buildbot.plugins import (
|
|
||||||
steps,
|
|
||||||
util,
|
|
||||||
)
|
|
||||||
from buildbot_nix.github.auth._type import (
|
|
||||||
AuthTypeLegacy,
|
|
||||||
AuthTypeApp,
|
|
||||||
)
|
)
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
'';
|
'';
|
||||||
configurators = [
|
configurators = [
|
||||||
''
|
''
|
||||||
|
@ -411,78 +411,65 @@ in
|
||||||
''
|
''
|
||||||
''
|
''
|
||||||
NixConfigurator(
|
NixConfigurator(
|
||||||
auth_backend=${builtins.toJSON cfg.authBackend},
|
BuildbotNixConfig.model_validate(json.loads(Path("${(pkgs.formats.json {}).generate "buildbot-nix-config.json" {
|
||||||
github=${
|
db_url = cfg.dbUrl;
|
||||||
if (!cfg.github.enable) then
|
auth_backend = cfg.authBackend;
|
||||||
"None"
|
build_retries = cfg.buildRetries;
|
||||||
|
cachix = if !cfg.cachix.enable then
|
||||||
|
null
|
||||||
else
|
else
|
||||||
"GithubConfig(
|
{
|
||||||
oauth_id=${builtins.toJSON cfg.github.oauthId},
|
name = cfg.cachix.name;
|
||||||
topic=${builtins.toJSON cfg.github.topic},
|
signing_key_file = cfg.cachix.signingKeyFile;
|
||||||
auth_type=${
|
auth_token_file = cfg.cachix.authTokenFile;
|
||||||
if cfg.github.authType ? "legacy" then
|
};
|
||||||
''AuthTypeLegacy()''
|
gitea = if !cfg.gitea.enable then
|
||||||
else if cfg.github.authType ? "app" then
|
null
|
||||||
''
|
|
||||||
AuthTypeApp(
|
|
||||||
app_id=${toString cfg.github.authType.app.id},
|
|
||||||
)
|
|
||||||
''
|
|
||||||
else
|
else
|
||||||
throw "One of AuthTypeApp or AuthTypeLegacy must be enabled"
|
{
|
||||||
|
token_file = "gitea-token";
|
||||||
|
webhook_secret_file = "gitea-webhook-secret";
|
||||||
|
project_cache_file = "gitea-project-cache.json";
|
||||||
|
oauth_secret_file = "gitea-oauth-secret";
|
||||||
|
instance_url = cfg.gitea.instanceUrl;
|
||||||
|
oauth_id = cfg.gitea.oauthId;
|
||||||
|
topic = cfg.gitea.topic;
|
||||||
|
};
|
||||||
|
github = if !cfg.github.enable then
|
||||||
|
null
|
||||||
|
else {
|
||||||
|
auth_type = if (cfg.github.authType ? "legacy") then
|
||||||
|
{
|
||||||
|
token_file = "github-token";
|
||||||
|
}
|
||||||
|
else if (cfg.github.authType ? "app") then
|
||||||
|
{
|
||||||
|
id = cfg.github.authType.app.id;
|
||||||
|
secret_key_file = cfg.github.authType.app.secretKeyFile;
|
||||||
|
installation_token_map_file = "github-app-installation-token-map.json";
|
||||||
|
project_id_map_file = "github-app-project-id-map-name.json";
|
||||||
|
jwt_token_map = "github-app-jwt-token";
|
||||||
}
|
}
|
||||||
)"
|
|
||||||
},
|
|
||||||
gitea=${
|
|
||||||
if !cfg.gitea.enable then
|
|
||||||
"None"
|
|
||||||
else
|
else
|
||||||
"GiteaConfig(
|
throw "authType is neither \"legacy\" nor \"app\"";
|
||||||
instance_url=${builtins.toJSON cfg.gitea.instanceUrl},
|
project_cache_file = "github-project-cache-v1.json";
|
||||||
oauth_id=${builtins.toJSON cfg.gitea.oauthId},
|
webhook_secret_file = "github-webhook-secret";
|
||||||
topic=${builtins.toJSON cfg.gitea.topic},
|
oauth_secret_file = "github-oauth-secret";
|
||||||
)"
|
oauth_id = cfg.github.oauthId;
|
||||||
},
|
topic = cfg.github.topic;
|
||||||
build_retries=${builtins.toJSON cfg.buildRetries},
|
};
|
||||||
cachix=${
|
admins = cfg.admins;
|
||||||
if cfg.cachix.name == null then
|
workers_file = cfg.workersFile;
|
||||||
"None"
|
build_systems = cfg.buildSystems;
|
||||||
else
|
eval_max_memory_size = cfg.evalMaxMemorySize;
|
||||||
"CachixConfig(
|
eval_worker_count = cfg.evalWorkerCount;
|
||||||
name=${builtins.toJSON cfg.cachix.name},
|
domain = cfg.domain;
|
||||||
signing_key_secret_name=${
|
webhook_base_url = cfg.webhookBaseUrl;
|
||||||
if cfg.cachix.signingKeyFile != null then builtins.toJSON "cachix-signing-key" else "None"
|
use_https = cfg.useHTTPS;
|
||||||
},
|
outputs_path = cfg.outputsPath;
|
||||||
auth_token_secret_name=${
|
url = config.services.buildbot-nix.master.webhookBaseUrl;
|
||||||
if cfg.cachix.authTokenFile != null then builtins.toJSON "cachix-auth-token" else "None"
|
post_build_steps = cfg.postBuildSteps;
|
||||||
},
|
}}").read_text()))
|
||||||
)"
|
|
||||||
},
|
|
||||||
admins=${builtins.toJSON cfg.admins},
|
|
||||||
url=${builtins.toJSON config.services.buildbot-nix.master.webhookBaseUrl},
|
|
||||||
nix_eval_max_memory_size=${builtins.toJSON cfg.evalMaxMemorySize},
|
|
||||||
nix_eval_worker_count=${
|
|
||||||
if cfg.evalWorkerCount == null then "None" else builtins.toString cfg.evalWorkerCount
|
|
||||||
},
|
|
||||||
nix_supported_systems=${builtins.toJSON cfg.buildSystems},
|
|
||||||
outputs_path=${if cfg.outputsPath == null then "None" else builtins.toJSON cfg.outputsPath},
|
|
||||||
post_build_steps=[
|
|
||||||
${lib.concatMapStringsSep ",\n" ({ name, environment, command }: ''
|
|
||||||
steps.ShellCommand(
|
|
||||||
name=${builtins.toJSON name},
|
|
||||||
env={
|
|
||||||
${lib.concatMapStringsSep ",\n" ({name, value}: ''
|
|
||||||
${name}: ${interpolateToString value}
|
|
||||||
'') (lib.mapAttrsToList lib.nameValuePair environment)}
|
|
||||||
},
|
|
||||||
command=[
|
|
||||||
${lib.concatMapStringsSep ",\n" (value:
|
|
||||||
interpolateToString value
|
|
||||||
) (if lib.isList command then command else [ command ])}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
'') cfg.postBuildSteps}
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
''
|
''
|
||||||
];
|
];
|
||||||
|
@ -509,6 +496,7 @@ in
|
||||||
});
|
});
|
||||||
in
|
in
|
||||||
ps: [
|
ps: [
|
||||||
|
ps.pydantic
|
||||||
pkgs.nix
|
pkgs.nix
|
||||||
ps.requests
|
ps.requests
|
||||||
ps.treq
|
ps.treq
|
||||||
|
@ -529,25 +517,20 @@ in
|
||||||
# in master.py we read secrets from $CREDENTIALS_DIRECTORY
|
# in master.py we read secrets from $CREDENTIALS_DIRECTORY
|
||||||
LoadCredential =
|
LoadCredential =
|
||||||
[ "buildbot-nix-workers:${cfg.workersFile}" ]
|
[ "buildbot-nix-workers:${cfg.workersFile}" ]
|
||||||
++ lib.optional (cfg.authBackend == "gitea") "gitea-oauth-secret:${cfg.gitea.oauthSecretFile}"
|
++ lib.optionals cfg.github.enable ([
|
||||||
++ lib.optional (cfg.authBackend == "github") "github-oauth-secret:${cfg.github.oauthSecretFile}"
|
|
||||||
++ lib.optional
|
|
||||||
(
|
|
||||||
cfg.cachix.signingKeyFile != null
|
|
||||||
) "cachix-signing-key:${builtins.toString cfg.cachix.signingKeyFile}"
|
|
||||||
++ lib.optional
|
|
||||||
(
|
|
||||||
cfg.cachix.authTokenFile != null
|
|
||||||
) "cachix-auth-token:${builtins.toString cfg.cachix.authTokenFile}"
|
|
||||||
++ lib.optionals (cfg.github.enable) ([
|
|
||||||
"github-webhook-secret:${cfg.github.webhookSecretFile}"
|
"github-webhook-secret:${cfg.github.webhookSecretFile}"
|
||||||
]
|
]
|
||||||
++ lib.optionals (cfg.github.authType ? "legacy") [
|
++ lib.optional (cfg.github.authType ? "legacy")
|
||||||
"github-token:${cfg.github.authType.legacy.tokenFile}"
|
"github-token:${cfg.github.authType.legacy.tokenFile}"
|
||||||
]
|
++ lib.optional (cfg.github.authType ? "app")
|
||||||
++ lib.optionals (cfg.github.authType ? "app") [
|
|
||||||
"github-app-secret-key:${cfg.github.authType.app.secretKeyFile}"
|
"github-app-secret-key:${cfg.github.authType.app.secretKeyFile}"
|
||||||
])
|
)
|
||||||
|
++ lib.optional (cfg.authBackend == "gitea") "gitea-oauth-secret:${cfg.gitea.oauthSecretFile}"
|
||||||
|
++ lib.optional (cfg.authBackend == "github") "github-oauth-secret:${cfg.github.oauthSecretFile}"
|
||||||
|
++ lib.optionals cfg.cachix.enable [
|
||||||
|
"cachix-signing-key:${builtins.toString cfg.cachix.signingKeyFile}"
|
||||||
|
"cachix-auth-token:${builtins.toString cfg.cachix.authTokenFile}"
|
||||||
|
]
|
||||||
++ lib.optionals cfg.gitea.enable [
|
++ lib.optionals cfg.gitea.enable [
|
||||||
"gitea-token:${cfg.gitea.tokenFile}"
|
"gitea-token:${cfg.gitea.tokenFile}"
|
||||||
"gitea-webhook-secret:${cfg.gitea.webhookSecretFile}"
|
"gitea-webhook-secret:${cfg.gitea.webhookSecretFile}"
|
||||||
|
|
|
@ -27,7 +27,6 @@ scripts = { buildbot-effects = "hercules_effects.cli:main" }
|
||||||
packages = [
|
packages = [
|
||||||
"buildbot_nix",
|
"buildbot_nix",
|
||||||
"buildbot_nix.github",
|
"buildbot_nix.github",
|
||||||
"buildbot_nix.github.auth",
|
|
||||||
"buildbot_effects"
|
"buildbot_effects"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue