diff --git a/modules/buildbot/buildbot_nix.py b/modules/buildbot/buildbot_nix.py new file mode 100644 index 0000000..3c97f03 --- /dev/null +++ b/modules/buildbot/buildbot_nix.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python3 + +import json +import multiprocessing +import os +import re +import uuid +from collections import defaultdict +from pathlib import Path +from typing import Any, Generator, List + +from buildbot.plugins import steps, util +from buildbot.process import buildstep, logobserver +from buildbot.process.properties import Properties +from buildbot.process.results import ALL_RESULTS, statusToString +from buildbot.steps.trigger import Trigger +from twisted.internet import defer + + +class BuildTrigger(Trigger): + """ + Dynamic trigger that creates a build for every attribute. + """ + + def __init__(self, scheduler: str, jobs: list[dict[str, str]], **kwargs): + if "name" not in kwargs: + kwargs["name"] = "trigger" + self.jobs = jobs + self.config = None + Trigger.__init__( + self, + waitForFinish=True, + schedulerNames=[scheduler], + haltOnFailure=True, + flunkOnFailure=True, + sourceStamps=[], + alwaysUseLatest=False, + updateSourceStamp=False, + **kwargs, + ) + + def createTriggerProperties(self, props): + return props + + def getSchedulersAndProperties(self): + build_props = self.build.getProperties() + repo_name = build_props.getProperty( + "github.base.repo.full_name", + build_props.getProperty("github.repository.full_name"), + ) + + sch = self.schedulerNames[0] + triggered_schedulers = [] + for job in self.jobs: + + attr = job.get("attr", "eval-error") + name = attr + if repo_name is not None: + name = f"{repo_name}: {name}" + drv_path = job.get("drvPath") + error = job.get("error") + out_path = job.get("outputs", {}).get("out") + + build_props.setProperty(f"{attr}-out_path", out_path, "nix-eval") + build_props.setProperty(f"{attr}-drv_path", drv_path, "nix-eval") + + props = Properties() + props.setProperty("virtual_builder_name", name, "nix-eval") + props.setProperty("virtual_builder_tags", "", "nix-eval") + props.setProperty("attr", attr, "nix-eval") + props.setProperty("drv_path", drv_path, "nix-eval") + props.setProperty("out_path", out_path, "nix-eval") + # we use this to identify builds when running a retry + props.setProperty("build_uuid", str(uuid.uuid4()), "nix-eval") + props.setProperty("error", error, "nix-eval") + triggered_schedulers.append((sch, props)) + return triggered_schedulers + + def getCurrentSummary(self): + """ + The original build trigger will the generic builder name `nix-build` in this case, which is not helpful + """ + if not self.triggeredNames: + return {"step": "running"} + summary = [] + if self._result_list: + for status in ALL_RESULTS: + count = self._result_list.count(status) + if count: + summary.append( + f"{self._result_list.count(status)} {statusToString(status, count)}" + ) + return {"step": f"({', '.join(summary)})"} + + +class NixEvalCommand(buildstep.ShellMixin, steps.BuildStep): + """ + Parses the output of `nix-eval-jobs` and triggers a `nix-build` build for + every attribute. + """ + + def __init__(self, **kwargs): + kwargs = self.setupShellMixin(kwargs) + super().__init__(**kwargs) + self.observer = logobserver.BufferLogObserver() + self.addLogObserver("stdio", self.observer) + + @defer.inlineCallbacks + def run(self) -> Generator[Any, object, Any]: + # run nix-instanstiate to generate the dict of stages + cmd = yield self.makeRemoteShellCommand() + yield self.runCommand(cmd) + + # if the command passes extract the list of stages + result = cmd.results() + if result == util.SUCCESS: + # create a ShellCommand for each stage and add them to the build + jobs = [] + + for line in self.observer.getStdout().split("\n"): + if line != "": + job = json.loads(line) + jobs.append(job) + self.build.addStepsAfterCurrentStep( + [BuildTrigger(scheduler="nix-build", name="nix-build", jobs=jobs)] + ) + + return result + + +# FIXME this leaks memory... but probably not enough that we care +class RetryCounter: + def __init__(self, retries: int) -> None: + self.builds: dict[uuid.UUID, int] = defaultdict(lambda: retries) + + def retry_build(self, id: uuid.UUID) -> int: + retries = self.builds[id] + if retries > 1: + self.builds[id] = retries - 1 + return retries + else: + return 0 + + +# For now we limit this to two. Often this allows us to make the error log +# shorter because we won't see the logs for all previous succeeded builds +RETRY_COUNTER = RetryCounter(retries=2) + + +class NixBuildCommand(buildstep.ShellMixin, steps.BuildStep): + """ + Builds a nix derivation if evaluation was successful, + otherwise this shows the evaluation error. + """ + + def __init__(self, **kwargs): + kwargs = self.setupShellMixin(kwargs) + super().__init__(**kwargs) + self.observer = logobserver.BufferLogObserver() + self.addLogObserver("stdio", self.observer) + + @defer.inlineCallbacks + def run(self) -> Generator[Any, object, Any]: + error = self.getProperty("error") + if error is not None: + attr = self.getProperty("attr") + # show eval error + self.build.results = util.FAILURE + log = yield self.addLog("nix_error") + log.addStderr(f"{attr} failed to evaluate:\n{error}") + return util.FAILURE + + # run `nix build` + cmd = yield self.makeRemoteShellCommand() + yield self.runCommand(cmd) + + res = cmd.results() + if res == util.FAILURE: + retries = RETRY_COUNTER.retry_build(self.getProperty("build_uuid")) + if retries > 0: + return util.RETRY + return res + + +class UpdateBuildOutput(steps.BuildStep): + """ + Updates store paths in a public www directory. + This is useful to prefetch updates without having to evaluate + on the target machine. + """ + + def __init__(self, branches: list[str], **kwargs): + self.branches = branches + super().__init__(**kwargs) + + def run(self) -> Generator[Any, object, Any]: + props = self.build.getProperties() + if props.getProperty("branch") not in self.branches: + return util.SKIPPED + attr = os.path.basename(props.getProperty("attr")) + out_path = props.getProperty("out_path") + # XXX don't hardcode this + p = Path("/var/www/buildbot/nix-outputs/") + os.makedirs(p, exist_ok=True) + with open(p / attr, "w") as f: + f.write(out_path) + return util.SUCCESS + + +class MergePr(steps.ShellCommand): + """ + Merge a pull request for specified branches and pull request owners + """ + + def __init__( + self, + base_branches: list[str], + owners: list[str], + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.base_branches = base_branches + self.owners = owners + self.observer = logobserver.BufferLogObserver() + self.addLogObserver("stdio", self.observer) + + @defer.inlineCallbacks + def reconfigService( + self, + base_branches: list[str], + owners: list[str], + **kwargs: Any, + ) -> Generator[Any, object, Any]: + self.base_branches = base_branches + self.owners = owners + super().reconfigService(**kwargs) + + @defer.inlineCallbacks + def run(self) -> Generator[Any, object, Any]: + props = self.build.getProperties() + if props.getProperty("basename") not in self.base_branches: + return util.SKIPPED + if props.getProperty("event") not in ["pull_request"]: + return util.SKIPPED + if not any(owner in self.owners for owner in props.getProperty("owners")): + return util.SKIPPED + + cmd = yield self.makeRemoteShellCommand() + yield self.runCommand(cmd) + return cmd.results() + + +class CreatePr(steps.ShellCommand): + """ + Creates a pull request if none exists + """ + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.addLogObserver( + "stdio", logobserver.LineConsumerLogObserver(self.check_pr_exists) + ) + + def check_pr_exists(self): + ignores = [ + re.compile( + """a pull request for branch ".*" into branch ".*" already exists:""" + ), + re.compile("No commits between .* and .*"), + ] + while True: + _, line = yield + if any(ignore.search(line) is not None for ignore in ignores): + self.skipped = True + + @defer.inlineCallbacks + def run(self): + self.skipped = False + cmd = yield self.makeRemoteShellCommand() + yield self.runCommand(cmd) + if self.skipped: + return util.SKIPPED + return cmd.results() + + +def nix_update_flake_config( + worker_names: list[str], + projectname: str, + github_token_secret: str, + github_bot_user: str, +) -> util.BuilderConfig: + """ + Updates the flake an opens a PR for it. + """ + factory = util.BuildFactory() + url_with_secret = util.Interpolate( + f"https://git:%(secret:{github_token_secret})s@github.com/{projectname}" + ) + factory.addStep( + steps.Git( + repourl=url_with_secret, + alwaysUseLatest=True, + method="clean", + submodules=True, + haltOnFailure=True, + ) + ) + factory.addStep( + steps.ShellCommand( + name="Update flakes", + env=dict( + GIT_AUTHOR_NAME=github_bot_user, + GIT_AUTHOR_EMAIL=f"{github_bot_user}@users.noreply.github.com", + GIT_COMMITTER_NAME=github_bot_user, + GIT_COMMITTER_EMAIL=f"{github_bot_user}@users.noreply.github.com", + ), + command=[ + "nix", + "flake", + "update", + "--commit-lock-file", + "--commit-lockfile-summary", + "flake.lock: Update", + ], + haltOnFailure=True, + ) + ) + factory.addStep( + steps.ShellCommand( + name="Force-Push to update_flake_lock branch", + command=[ + "git", + "push", + "--force", + "origin", + "HEAD:refs/heads/update_flake_lock", + ], + haltOnFailure=True, + ) + ) + factory.addStep( + CreatePr( + name="Create pull-request", + env=dict(GITHUB_TOKEN=util.Secret(github_token_secret)), + command=[ + "gh", + "pr", + "create", + "--repo", + projectname, + "--title", + "flake.lock: Update", + "--body", + "Automatic buildbot update", + "--head", + "refs/heads/update_flake_lock", + "--base", + "main", + ], + haltOnFailure = True + ) + ) + return util.BuilderConfig( + name="nix-update-flake", + workernames=worker_names, + factory=factory, + properties=dict(virtual_builder_name="nix-update-flake"), + ) + + +class Machine: + def __init__(self, hostname: str, attr_name: str) -> None: + self.hostname = hostname + self.attr_name = attr_name + + +class DeployTrigger(Trigger): + """ + Dynamic trigger that creates a deploy step for every machine. + """ + + def __init__(self, scheduler: str, machines: list[Machine], **kwargs): + if "name" not in kwargs: + kwargs["name"] = "trigger" + self.machines = machines + self.config = None + Trigger.__init__( + self, + waitForFinish=True, + schedulerNames=[scheduler], + haltOnFailure=True, + flunkOnFailure=True, + sourceStamps=[], + alwaysUseLatest=False, + updateSourceStamp=False, + **kwargs, + ) + + def createTriggerProperties(self, props): + return props + + def getSchedulersAndProperties(self): + build_props = self.build.getProperties() + repo_name = build_props.getProperty( + "github.base.repo.full_name", + build_props.getProperty("github.repository.full_name"), + ) + + sch = self.schedulerNames[0] + + triggered_schedulers = [] + for m in self.machines: + out_path = build_props.getProperty(f"nixos-{m.attr_name}-out_path") + props = Properties() + name = m.attr_name + if repo_name is not None: + name = f"{repo_name}: Deploy {name}" + props.setProperty("virtual_builder_name", name, "deploy") + props.setProperty("attr", m.attr_name, "deploy") + props.setProperty("out_path", out_path, "deploy") + triggered_schedulers.append((sch, props)) + return triggered_schedulers + + @defer.inlineCallbacks + def run(self): + props = self.build.getProperties() + if props.getProperty("branch") not in self.branches: + return util.SKIPPED + res = yield super().__init__() + return res + + def getCurrentSummary(self): + """ + The original build trigger will the generic builder name `nix-build` in this case, which is not helpful + """ + if not self.triggeredNames: + return {"step": "running"} + summary = [] + if self._result_list: + for status in ALL_RESULTS: + count = self._result_list.count(status) + if count: + summary.append( + f"{self._result_list.count(status)} {statusToString(status, count)}" + ) + return {"step": f"({', '.join(summary)})"} + + +def nix_eval_config( + worker_names: list[str], + github_token_secret: str, + automerge_users: List[str] = [], + machines: list[Machine] = [], +) -> util.BuilderConfig: + """ + Uses nix-eval-jobs to evaluate hydraJobs from flake.nix in parallel. + For each evaluated attribute a new build pipeline is started. + If all builds succeed and the build was for a PR opened by the flake update bot, + this PR is merged. + """ + factory = util.BuildFactory() + # check out the source + url_with_secret = util.Interpolate( + f"https://git:%(secret:{github_token_secret})s@github.com/%(prop:project)s" + ) + factory.addStep( + steps.Git( + repourl=url_with_secret, + method="clean", + submodules=True, + haltOnFailure=True, + ) + ) + + factory.addStep( + NixEvalCommand( + env={}, + name="Eval flake", + command=[ + "nix-eval-jobs", + "--workers", + 8, + "--option", + "accept-flake-config", + "true", + "--gc-roots-dir", + # FIXME: don't hardcode this + "/var/lib/buildbot-worker/gcroot", + "--flake", + ".#hydraJobs", + ], + haltOnFailure=True, + ) + ) + # Merge flake-update pull requests if CI succeeds + if len(automerge_users) > 0: + factory.addStep( + MergePr( + name="Merge pull-request", + env=dict(GITHUB_TOKEN=util.Secret(github_token_secret)), + base_branches=["master"], + owners=automerge_users, + command=[ + "gh", + "pr", + "merge", + "--repo", + util.Property("project"), + "--rebase", + util.Property("pullrequesturl"), + ], + ) + ) + + # factory.addStep( + # DeployTrigger(scheduler="nixos-deploy", name="nixos-deploy", machines=machines) + # ) + # factory.addStep( + # DeployNixOS( + # name="Deploy NixOS machines", + # env=dict(GITHUB_TOKEN=util.Secret(github_token_secret)), + # base_branches=["master"], + # owners=automerge_users, + # command=[ + # "gh", + # "pr", + # "merge", + # "--repo", + # util.Property("project"), + # "--rebase", + # util.Property("pullrequesturl"), + # ], + # ) + # ) + + return util.BuilderConfig( + name="nix-eval", + workernames=worker_names, + factory=factory, + properties=dict(virtual_builder_name="nix-eval"), + ) + + +def nix_build_config( + worker_names: list[str], + has_cachix_auth_token: bool = False, + has_cachix_signing_key: bool = False, +) -> util.BuilderConfig: + """ + Builds one nix flake attribute. + """ + factory = util.BuildFactory() + factory.addStep( + NixBuildCommand( + env={}, + name="Build flake attr", + command=[ + "nix-build", + "--option", + "keep-going", + "true", + "--accept-flake-config", + "--out-link", + util.Interpolate("result-%(prop:attr)s"), + util.Property("drv_path"), + ], + haltOnFailure=True, + ) + ) + if has_cachix_auth_token or has_cachix_signing_key: + if has_cachix_signing_key: + env = dict(CACHIX_SIGNING_KEY=util.Secret("cachix-signing-key")) + else: + env = dict(CACHIX_AUTH_TOKEN=util.Secret("cachix-auth-token")) + factory.addStep( + steps.ShellCommand( + name="Upload cachix", + env=env, + command=[ + "cachix", + "push", + util.Secret("cachix-name"), + util.Interpolate("result-%(prop:attr)s"), + ], + ) + ) + factory.addStep(UpdateBuildOutput(name="Update build output", branches=["master"])) + return util.BuilderConfig( + name="nix-build", + workernames=worker_names, + properties=[], + collapseRequests=False, + env={}, + factory=factory, + ) + + +# def nixos_deployment_config(worker_names: list[str]) -> util.BuilderConfig: +# factory = util.BuildFactory() +# factory.addStep( +# NixBuildCommand( +# env={}, +# name="Deploy NixOS", +# command=[ +# "nix", +# "build", +# "--option", +# "keep-going", +# "true", +# "-L", +# "--out-link", +# util.Interpolate("result-%(prop:attr)s"), +# util.Property("drv_path"), +# ], +# haltOnFailure=True, +# ) +# ) +# return util.BuilderConfig( +# name="nix-build", +# workernames=worker_names, +# properties=[], +# collapseRequests=False, +# env={}, +# factory=factory, +# ) + diff --git a/modules/buildbot/default.nix b/modules/buildbot/default.nix new file mode 100644 index 0000000..87701d9 --- /dev/null +++ b/modules/buildbot/default.nix @@ -0,0 +1,149 @@ +{ lib, pkgs, config, ... }: +with lib; +let + cfg = config.luj.buildbot; + port = "1810"; + package = pkgs.python3Packages.buildbot-worker; + python = package.pythonModule; + home = "/var/lib/buildbot-worker"; + buildbotDir = "${home}/worker"; +in +{ + + options.luj.buildbot = { + enable = mkEnableOption "activate buildbot service"; + + nginx.enable = mkEnableOption "activate nginx"; + nginx.subdomain = mkOption { + type = types.str; + }; + + }; + + config = mkIf cfg.enable { + + # Buildbot master + + services.buildbot-master = { + enable = true; + masterCfg = "${./.}/master.py"; + pythonPackages = ps: [ + ps.requests + ps.treq + ps.psycopg2 + ps.buildbot-worker + ]; + }; + + systemd.services.buildbot-master = { + reloadIfChanged = true; + environment = { + PORT = port; + # Github app used for the login button + GITHUB_OAUTH_ID = "355493f668a8e1aa10cf"; + GITHUB_ORG = "JulienMalka"; + GITHUB_REPO = "nix-config"; + + BUILDBOT_URL = "https://buildbot.julienmalka.me/"; + BUILDBOT_GITHUB_USER = "JulienMalka"; + # comma seperated list of users that are allowed to login to buildbot and do stuff + GITHUB_ADMINS = "JulienMalka"; + }; + serviceConfig = { + # Restart buildbot with a delay. This time way we can use buildbot to deploy itself. + ExecReload = "+${pkgs.systemd}/bin/systemd-run --on-active=60 ${pkgs.systemd}/bin/systemctl restart buildbot-master"; + # in master.py we read secrets from $CREDENTIALS_DIRECTORY + LoadCredential = [ + "github-token:${config.sops.secrets.github-token.path}" + "github-webhook-secret:${config.sops.secrets.github-webhook-secret.path}" + "github-oauth-secret:${config.sops.secrets.github-oauth-secret.path}" + "buildbot-nix-workers:${config.sops.secrets.buildbot-nix-workers.path}" + ]; + }; + }; + sops.secrets = { + github-token = { + format = "binary"; + sopsFile = ../../secrets/github-token-secret; + }; + github-webhook-secret = { + format = "binary"; + sopsFile = ../../secrets/github-webhook-secret; + }; + github-oauth-secret = { + format = "binary"; + sopsFile = ../../secrets/github-oauth-secret; + }; + buildbot-nix-workers = { + format = "binary"; + sopsFile = ../../secrets/buildbot-nix-workers; + }; + }; + + services.nginx.virtualHosts."buildbot.julienmalka.me" = + { + locations."/".proxyPass = "http://127.0.0.1:1810/"; + locations."/sse" = { + proxyPass = "http://127.0.0.1:1810/sse/"; + # proxy buffering will prevent sse to work + extraConfig = "proxy_buffering off;"; + }; + locations."/ws" = { + proxyPass = "http://127.0.0.1:1810/ws"; + proxyWebsockets = true; + # raise the proxy timeout for the websocket + extraConfig = "proxy_read_timeout 6000s;"; + }; + }; + + #buildbot worker + + nix.settings.allowed-users = [ "buildbot-worker" ]; + users.users.buildbot-worker = { + description = "Buildbot Worker User."; + isSystemUser = true; + createHome = true; + home = "/var/lib/buildbot-worker"; + group = "buildbot-worker"; + useDefaultShell = true; + }; + users.groups.buildbot-worker = { }; + + systemd.services.buildbot-worker = { + reloadIfChanged = true; + description = "Buildbot Worker."; + after = [ "network.target" "buildbot-master.service" ]; + wantedBy = [ "multi-user.target" ]; + path = [ + pkgs.unstable.nix-eval-jobs + pkgs.git + pkgs.gh + pkgs.nix + pkgs.nix-output-monitor + ]; + environment.PYTHONPATH = "${python.withPackages (_: [package])}/${python.sitePackages}"; + environment.MASTER_URL = ''tcp:host=localhost:port=9989''; + environment.BUILDBOT_DIR = buildbotDir; + environment.WORKER_PASSWORD_FILE = config.sops.secrets.buildbot-nix-worker-password.path; + + serviceConfig = { + Type = "simple"; + User = "buildbot-worker"; + Group = "buildbot-worker"; + WorkingDirectory = home; + + # Restart buildbot with a delay. This time way we can use buildbot to deploy itself. + ExecReload = "+${pkgs.systemd}/bin/systemd-run --on-active=60 ${pkgs.systemd}/bin/systemctl restart buildbot-worker"; + ExecStart = "${python.pkgs.twisted}/bin/twistd --nodaemon --pidfile= --logfile - --python ${./worker.py}"; + }; + }; + sops.secrets.buildbot-nix-worker-password = { + format = "binary"; + owner = "buildbot-worker"; + sopsFile = ../../secrets/buildbot-nix-worker-password; + }; + + + }; +} + diff --git a/modules/buildbot/master.py b/modules/buildbot/master.py new file mode 100644 index 0000000..1075379 --- /dev/null +++ b/modules/buildbot/master.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 + +import json +import os +import sys +from datetime import timedelta +from pathlib import Path +from typing import Any + +from buildbot.plugins import reporters, schedulers, secrets, util, worker +from buildbot.process.properties import Interpolate + +# allow to import modules +sys.path.append(str(Path(__file__).parent)) + +from buildbot_nix import ( + nix_build_config, + nix_eval_config, + nix_update_flake_config, +) + +def read_secret_file(secret_name: str) -> str: + directory = os.environ.get("CREDENTIALS_DIRECTORY") + if directory is None: + print("directory not set", file=sys.stderr) + sys.exit(1) + return Path(directory).joinpath(secret_name).read_text() + + +ORG = os.environ["GITHUB_ORG"] +REPO = os.environ["GITHUB_REPO"] +BUILDBOT_URL = os.environ["BUILDBOT_URL"] +BUILDBOT_GITHUB_USER = os.environ["BUILDBOT_GITHUB_USER"] + + +def build_config() -> dict[str, Any]: + c = {} + c["buildbotNetUsageData"] = None + print(ORG, REPO) + + # configure a janitor which will delete all logs older than one month, and will run on sundays at noon + c["configurators"] = [ + util.JanitorConfigurator(logHorizon=timedelta(weeks=4), hour=12, dayOfWeek=6) + ] + + c["schedulers"] = [ + # build all pushes to default branch + schedulers.SingleBranchScheduler( + name="main", + change_filter=util.ChangeFilter( + repository=f"https://github.com/{ORG}/{REPO}", + filter_fn=lambda c: c.branch + == c.properties.getProperty("github.repository.default_branch"), + ), + builderNames=["nix-eval"], + ), + # build all pull requests + schedulers.SingleBranchScheduler( + name="prs", + change_filter=util.ChangeFilter( + repository=f"https://github.com/{ORG}/{REPO}", category="pull" + ), + builderNames=["nix-eval"], + ), + schedulers.SingleBranchScheduler( + name="flake-sources", + change_filter=util.ChangeFilter( + repository=f"https://github.com/{ORG}/nixpkgs", branch="main" + ), + treeStableTimer=20, + builderNames=["nix-update-flake"], + ), + # this is triggered from `nix-eval` + schedulers.Triggerable( + name="nix-build", + builderNames=["nix-build"], + ), + # allow to manually trigger a nix-build + schedulers.ForceScheduler(name="force", builderNames=["nix-eval"]), + # allow to manually update flakes + schedulers.ForceScheduler( + name="update-flake", + builderNames=["nix-update-flake"], + buttonName="Update flakes", + ), + # updates flakes once a weeek + schedulers.NightlyTriggerable( + name="update-flake-weekly", + builderNames=["nix-update-flake"], + hour=3, + minute=0, + dayOfWeek=6, + ), + ] + + github_api_token = read_secret_file("github-token") + c["services"] = [ + reporters.GitHubStatusPush( + token=github_api_token, + # Since we dynamically create build steps, + # we use `virtual_builder_name` in the webinterface + # so that we distinguish what has beeing build + context=Interpolate("buildbot/%(prop:virtual_builder_name)s"), + ), + ] + + # Shape of this file: + # [ { "name": "<worker-name>", "pass": "<worker-password>", "cores": "<cpu-cores>" } ] + worker_config = json.loads(read_secret_file("buildbot-nix-workers")) + + credentials = os.environ.get("CREDENTIALS_DIRECTORY", ".") + + has_cachix_auth_token = False + has_cachix_signing_key = False + + systemd_secrets = secrets.SecretInAFile(dirname=credentials) + c["secretsProviders"] = [systemd_secrets] + c["workers"] = [] + worker_names = [] + for item in worker_config: + print(f"WORKER : {item}") + cores = item.get("cores", 0) + for i in range(cores): + worker_name = f"{item['name']}-{i}" + c["workers"].append(worker.Worker(worker_name, item["pass"])) + worker_names.append(worker_name) + c["builders"] = [ + # Since all workers run on the same machine, we only assign one of them to do the evaluation. + # This should prevent exessive memory usage. + nix_eval_config( + [worker_names[0]], + github_token_secret="github-token", + automerge_users=[BUILDBOT_GITHUB_USER], + ), + nix_build_config(worker_names, has_cachix_auth_token, has_cachix_signing_key), + nix_update_flake_config( + worker_names, + f"{ORG}/{REPO}", + github_token_secret="github-token", + github_bot_user=BUILDBOT_GITHUB_USER, + ), + ] + + github_admins = os.environ.get("GITHUB_ADMINS", "").split(",") + + print(github_admins) + + print(os.environ.get("GITHUB_OAUTH_ID")) + print(read_secret_file("github-oauth-secret")) + print("lol") + print(read_secret_file("github-webhook-secret")) + print(github_api_token) + + c["www"] = { + "avatar_methods": [util.AvatarGitHub()], + "port": int(os.environ.get("PORT", "1810")), + "auth": util.UserPasswordAuth({"JulienMalka": "hello"}), + "authz": util.Authz( + roleMatchers=[ + util.RolesFromUsername(roles=["admin"], usernames=github_admins) + ], + allowRules=[ + util.AnyEndpointMatcher(role="admin", defaultDeny=False), + util.AnyControlEndpointMatcher(role="admins"), + ], + ), + "plugins": dict(waterfall_view={}, console_view={}, grid_view={}), + "change_hook_dialects": dict( + github={ + "secret": "hello", + "strict": False, + "token": github_api_token, + "github_property_whitelist": "*", + } + ), + } + + c["db"] = {"db_url": os.environ.get("DB_URL", "sqlite:///state.sqlite")} + + c["protocols"] = {"pb": {"port": "tcp:9989:interface=\\:\\:"}} + c["buildbotURL"] = BUILDBOT_URL + + return c + + +BuildmasterConfig = build_config() + diff --git a/modules/buildbot/worker.py b/modules/buildbot/worker.py new file mode 100644 index 0000000..972825f --- /dev/null +++ b/modules/buildbot/worker.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +import multiprocessing +import os +import socket +from io import open + +from buildbot_worker.bot import Worker +from twisted.application import service + + +def require_env(key: str) -> str: + val = os.environ.get(key) + assert val is not None, "val is not set" + return val + + +def setup_worker(application: service.Application, id: int) -> None: + basedir = f"{require_env('BUILDBOT_DIR')}-{id}" + os.makedirs(basedir, mode=0o700, exist_ok=True) + + master_url = require_env("MASTER_URL") + hostname = socket.gethostname() + workername = f"{hostname}-{id}" + + with open( + require_env("WORKER_PASSWORD_FILE"), "r", encoding="utf-8" + ) as passwd_file: + passwd = passwd_file.read().strip("\r\n") + keepalive = 600 + umask = None + maxdelay = 300 + numcpus = None + allow_shutdown = None + + s = Worker( + None, + None, + workername, + passwd, + basedir, + keepalive, + connection_string=master_url, + umask=umask, + maxdelay=maxdelay, + numcpus=numcpus, + allow_shutdown=allow_shutdown, + ) + s.setServiceParent(application) + + +# note: this line is matched against to check that this is a worker +# directory; do not edit it. +application = service.Application("buildbot-worker") + +for i in range(8): + setup_worker(application, i) +