diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..08805b5f --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,5 @@ +/.venv +/build +/dist +/*.egg-info +/*.dist-info diff --git a/server/README.rst b/server/README.rst index f0eddd86..e708b7ab 100644 --- a/server/README.rst +++ b/server/README.rst @@ -1,31 +1,8 @@ -============== -Upload Servers -============== +================= +Package Processor +================= -There are three: apt, RPM, and a generic package container with .jsonindexes. +This package is sourced by genrepo_ to process and publish packages uploaded +by release workflows. -They are Docker containers that accept new files and process the relevant -S3 bucket to serve the updated database. - -Generic package is not much more than an FTP server but apt and RPM are -databases with their own package listings, indexes, and encoded file -locations. - -To test locally:: - - $ docker build -t ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-2.amazonaws.com/genrepo:latest containers/genrepo - $ docker run -it --env-file=.env --rm -p 2222:22 --name=genrepo ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-2.amazonaws.com/genrepo:latest - -To upload a new package:: - - $ aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-2.amazonaws.com - $ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.us-east-2.amazonaws.com/genrepo:latest - -To kick the server to upgrade using the new uploaded package:: - - $ edbcloud fargate edgedbeng pkg-genrepo --force - -(without --force it edbcloud assumes that nothing changed) - -Analogic actions for aptrepo and rpmrepo, just replace "genrepo" in the -commands above with the respective repository name. +.. _genrepo: https://github.com/edgedb/infra/tree/main/devops/services/genrepo diff --git a/server/build-images.sh b/server/build-images.sh deleted file mode 100755 index 70ea5afe..00000000 --- a/server/build-images.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh - -set -e - -if [ -z ${AWS_ACCOUNT+x} ]; then - echo "Set \$AWS_ACCOUNT to use this." - exit 1 -fi - -AWS_URL="${AWS_ACCOUNT}.dkr.ecr.us-east-2.amazonaws.com" - -# Requires local credentials to be present or the use of the -# aws-actions/configure-aws-credentials@v1 GitHub action. -aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin $AWS_URL - -set -ex - -containers="$(dirname $0)/containers/" - -for container in $(ls "${containers}"); do - repo="${AWS_URL}/${container}" - tag="latest" - docker build -t "${repo}:${tag}" "${containers}/${container}" - docker push "${repo}" -done diff --git a/server/containers/aptrepo/Dockerfile b/server/containers/aptrepo/Dockerfile deleted file mode 100644 index f449d11f..00000000 --- a/server/containers/aptrepo/Dockerfile +++ /dev/null @@ -1,63 +0,0 @@ -FROM debian:buster - -ENV REPREPRO_BASE_DIR=/var/tmp/repo/local/ -ENV REPREPRO_CONFIG_DIR=/etc/reprepro/ -ENV REPREPRO_SPOOL_DIR=/var/spool/reprepro -ENV REPREPRO_INCOMING_TMP_DIR=/var/tmp/incoming-staging/ - -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive \ - apt-get install -y --no-install-recommends \ - reprepro openssh-server bash inoticoming gosu gnupg \ - gcc python3-dev python3-setuptools python3-pip libffi-dev \ - && pip3 install \ - awscli \ - boto3 \ - click \ - typing-extensions \ - && addgroup --gid 2000 reprepro \ - && adduser --uid 2000 --gid 2000 reprepro \ - && addgroup uploaders \ - && addgroup incoming \ - && adduser --ingroup uploaders \ - --home $REPREPRO_SPOOL_DIR --no-create-home uploader \ - && adduser reprepro incoming \ - && adduser uploader incoming \ - && mkdir -p $REPREPRO_SPOOL_DIR/incoming \ - && chown reprepro:incoming $REPREPRO_SPOOL_DIR/incoming \ - && chmod g+ws $REPREPRO_SPOOL_DIR/incoming \ - && mkdir -p $REPREPRO_INCOMING_TMP_DIR \ - && chown reprepro:reprepro $REPREPRO_INCOMING_TMP_DIR \ - && mkdir -p /etc/ssh/authorized_keys/ \ - && mkdir -p ~root/.ssh && chmod 700 ~root/.ssh/ \ - && mkdir -p /etc/ssh.default/ \ - && rm -rf /var/lib/apt/lists/* - -COPY config/reprepro/* /etc/reprepro/ -COPY config/sshd/* /etc/ssh.default/ -COPY config/gnupg/* /home/reprepro/.gnupg/ -COPY entrypoint.sh /entrypoint.sh -COPY fetch_secrets.py /usr/local/bin/ -COPY processincoming.sh /usr/local/bin/ -COPY makeindex.py /usr/local/bin/ -COPY visor.py /usr/local/bin/ -RUN sed -i -e "s|%%REPREPRO_BASE_DIR%%|${REPREPRO_BASE_DIR}|g" \ - /usr/local/bin/processincoming.sh \ - && sed -i -e "s|%%REPREPRO_SPOOL_DIR%%|${REPREPRO_SPOOL_DIR}|g" \ - /etc/reprepro/incoming \ - && sed -i -e "s|%%REPREPRO_INCOMING_TMP_DIR%%|${REPREPRO_INCOMING_TMP_DIR}|g" \ - /etc/reprepro/incoming \ - && sed -i -e "s|%%REPREPRO_SPOOL_DIR%%|${REPREPRO_SPOOL_DIR}|g" \ - /etc/ssh.default/sshd_config_conditional - -RUN chown -R reprepro:reprepro /home/reprepro/.gnupg \ - && chmod 700 /home/reprepro/.gnupg \ - && chmod 600 /home/reprepro/.gnupg/* - -EXPOSE 22 - -VOLUME /var/tmp/repo - -ENTRYPOINT ["/entrypoint.sh"] - -CMD ["/usr/sbin/sshd", "-e", "-D", "-f", "/etc/ssh.default/sshd_config"] diff --git a/server/containers/aptrepo/config/gnupg/gpg-agent.conf b/server/containers/aptrepo/config/gnupg/gpg-agent.conf deleted file mode 100644 index d1b6ae31..00000000 --- a/server/containers/aptrepo/config/gnupg/gpg-agent.conf +++ /dev/null @@ -1 +0,0 @@ -allow-loopback-pinentry diff --git a/server/containers/aptrepo/config/gnupg/gpg.conf b/server/containers/aptrepo/config/gnupg/gpg.conf deleted file mode 100644 index 065d80c7..00000000 --- a/server/containers/aptrepo/config/gnupg/gpg.conf +++ /dev/null @@ -1 +0,0 @@ -pinentry-mode loopback diff --git a/server/containers/aptrepo/config/reprepro/distributions b/server/containers/aptrepo/config/reprepro/distributions deleted file mode 100644 index 44c4af6a..00000000 --- a/server/containers/aptrepo/config/reprepro/distributions +++ /dev/null @@ -1,53 +0,0 @@ -Origin: EdgeDB Open Source Project -Label: EdgeDB -Suite: stable -Codename: stretch -Architectures: amd64 source -Components: main nightly -Description: EdgeDB Package Repository for Debian Stretch -SignWith: A3FF3633DF29BDBF - -Origin: EdgeDB Open Source Project -Label: EdgeDB -Suite: stable -Codename: buster -Architectures: amd64 source -Components: main nightly -Description: EdgeDB Package Repository for Debian Buster -SignWith: A3FF3633DF29BDBF - -Origin: EdgeDB Open Source Project -Label: EdgeDB -Suite: stable -Codename: bullseye -Architectures: amd64 source -Components: main nightly -Description: EdgeDB Package Repository for Debian Bullseye -SignWith: A3FF3633DF29BDBF - -Origin: EdgeDB Open Source Project -Label: EdgeDB -Suite: stable -Codename: xenial -Architectures: amd64 source -Components: main nightly -Description: EdgeDB Package Repository for Ubuntu 16.04 -SignWith: A3FF3633DF29BDBF - -Origin: EdgeDB Open Source Project -Label: EdgeDB -Suite: stable -Codename: bionic -Architectures: amd64 source -Components: main nightly -Description: EdgeDB Package Repository for Ubuntu 18.04 -SignWith: A3FF3633DF29BDBF - -Origin: EdgeDB Open Source Project -Label: EdgeDB -Suite: stable -Codename: focal -Architectures: amd64 source -Components: main nightly -Description: EdgeDB Package Repository for Ubuntu 20.04 -SignWith: A3FF3633DF29BDBF diff --git a/server/containers/aptrepo/config/reprepro/incoming b/server/containers/aptrepo/config/reprepro/incoming deleted file mode 100644 index dff4bd0d..00000000 --- a/server/containers/aptrepo/config/reprepro/incoming +++ /dev/null @@ -1,4 +0,0 @@ -Name: default -IncomingDir: %%REPREPRO_SPOOL_DIR%%/incoming -TempDir: %%REPREPRO_INCOMING_TMP_DIR%% -Allow: stretch buster bullseye xenial bionic focal diff --git a/server/containers/aptrepo/config/sshd/sshd_config b/server/containers/aptrepo/config/sshd/sshd_config deleted file mode 100644 index 1fc796f3..00000000 --- a/server/containers/aptrepo/config/sshd/sshd_config +++ /dev/null @@ -1,6 +0,0 @@ -StrictModes no -PasswordAuthentication no -Subsystem sftp /usr/lib/openssh/sftp-server -AcceptEnv LANG LC_* -PrintMotd no -UsePAM yes diff --git a/server/containers/aptrepo/config/sshd/sshd_config_conditional b/server/containers/aptrepo/config/sshd/sshd_config_conditional deleted file mode 100644 index 9d030b64..00000000 --- a/server/containers/aptrepo/config/sshd/sshd_config_conditional +++ /dev/null @@ -1,7 +0,0 @@ -Match Group uploaders - ChrootDirectory %%REPREPRO_SPOOL_DIR%% - ForceCommand internal-sftp - AllowTcpForwarding no - PermitTunnel no - X11Forwarding no - AuthorizedKeysFile /etc/ssh/authorized_keys/uploaders diff --git a/server/containers/aptrepo/entrypoint.sh b/server/containers/aptrepo/entrypoint.sh deleted file mode 100755 index eaf342dc..00000000 --- a/server/containers/aptrepo/entrypoint.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash - -set -e - -[ "$DEBUG" == 'true' ] && set -x - -echo REPREPRO_CONFIG_DIR=${REPREPRO_CONFIG_DIR} >> /etc/environment - -mkdir -p "${REPREPRO_BASE_DIR}" -chown -R reprepro:reprepro "${REPREPRO_BASE_DIR}" - -if [ -w ~/.ssh ]; then - chown root:root ~/.ssh && chmod 700 ~/.ssh/ -fi - -fetch_secrets.py client-keys-root /root/.ssh -f authorized_keys -fetch_secrets.py client-keys-uploaders /etc/ssh/authorized_keys -f uploaders - -if [ -w ~/.ssh/authorized_keys ]; then - chown root:root ~/.ssh/authorized_keys - chmod 400 ~/.ssh/authorized_keys -fi - -fetch_secrets.py server-host-key- /etc/ssh/ - -if ls /etc/ssh/server-host-key-* 1> /dev/null 2>&1; then - echo "Found shared ssh host keys in /etc/ssh/" - SSH_KEY_WILDCARD="server-host-key-*" -elif ls /etc/ssh/ssh_host_* 1> /dev/null 2>&1; then - echo "Found custom ssh host keys in /etc/ssh/" - SSH_KEY_WILDCARD="ssh_host_*_key" -else - echo "No ssh host keys found in /etc/ssh. Generating." - ssh-keygen -A - SSH_KEY_WILDCARD="ssh_host_*_key" -fi - -if [ "$DEBUG" == 'true' ]; then - echo "sshd_config" - echo "-----------" - cat "/etc/ssh.default/sshd_config" -fi - -while IFS= read -r -d '' path; do - echo HostKey "${path}" >> "/etc/ssh.default/sshd_config" - if [ -w "${path}" ]; then - chown root:root "${path}" - chmod 400 "${path}" - fi -done < <(find "/etc/ssh/" -name $SSH_KEY_WILDCARD -print0) - -if [ "$DEBUG" == 'true' ]; then - echo "LogLevel DEBUG2" >> "/etc/ssh.default/sshd_config" -fi - -fetch_secrets.py release-signing- /root/gpg-keys/ - -if [ -e "/root/gpg-keys/" ]; then - while IFS= read -r -d '' path; do - cat "${path}" | gosu reprepro:reprepro gpg --import - done < <(find "/root/gpg-keys/" -maxdepth 1 -type f -print0) -fi - -chown -R reprepro:reprepro "${REPREPRO_BASE_DIR}" - -if [ "${AWS_ACCESS_KEY_ID}" != "" ]; then - mkdir -p /home/reprepro/.aws - echo "[default]" >/home/reprepro/.aws/credentials - echo "aws_access_key_id = ${AWS_ACCESS_KEY_ID}" >>/home/reprepro/.aws/credentials - echo "aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" >>/home/reprepro/.aws/credentials - chown -R reprepro:reprepro /home/reprepro/.aws - chmod 400 /home/reprepro/.aws/credentials -fi - -gosu reprepro:reprepro aws s3 sync --exact-timestamps --delete \ - s3://edgedb-packages/apt/ "${REPREPRO_BASE_DIR}/" - -if [ -n "${PORT}" ]; then - echo "Port ${PORT}" >> "/etc/ssh.default/sshd_config" -else - echo "Port 22" >> "/etc/ssh.default/sshd_config" -fi - -# Conditional blocks go last -cat "/etc/ssh.default/sshd_config_conditional" >> \ - "/etc/ssh.default/sshd_config" - -mkdir -p /var/run/sshd - -if [ "$(basename $1)" == "sshd" ]; then - if [ "$DEBUG" == 'true' ]; then - echo "sshd_config" - echo "-----------" - cat "/etc/ssh.default/sshd_config" - fi - export PYTHONUNBUFFERED=1 - /usr/local/bin/visor.py << EOF - [sshd] - cmd = $@ - [inoticoming] - user = reprepro - cmd = - inoticoming - --initialsearch - --foreground - ${REPREPRO_SPOOL_DIR}/incoming - --suffix - ".changes" - --chdir - "${REPREPRO_SPOOL_DIR}" - /usr/local/bin/processincoming.sh - {} - \; -EOF -else - exec "$@" -fi diff --git a/server/containers/aptrepo/fetch_secrets.py b/server/containers/aptrepo/fetch_secrets.py deleted file mode 100755 index 22e6e48a..00000000 --- a/server/containers/aptrepo/fetch_secrets.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations -from typing import * - -import functools -import os -import pathlib - -import boto3 -from botocore.exceptions import ClientError -import click - - -APP_PREFIX = "edbcloud/app/edgedbeng/pkg/" - - -def list_secret_ids_by_prefix( - client: boto3.SecretsManagerClient, - secret_id_prefix: str, -) -> Set[str]: - paginator = client.get_paginator("list_secrets") - secret_ids = set() - - try: - for page in paginator.paginate( - Filters=[ - { - "Key": "name", - "Values": [f"{APP_PREFIX}{secret_id_prefix}"], - }, - ], - ): - for secret in page["SecretList"]: - secret_ids.add(secret["Name"]) - except ClientError as e: - code = e.response["Error"]["Code"] - if code == "DecryptionFailureException": - # Secrets Manager can't decrypt the protected secret text using - # the provided KMS key. - raise e - elif code == "InternalServiceErrorException": - # An error occurred on the server side. - raise e - elif code == "InvalidParameterException": - # You provided an invalid value for a parameter. - raise e - elif code == "InvalidRequestException": - # You provided a parameter value that is not valid for the current - # state of the resource. - raise e - elif code == "ResourceNotFoundException": - # We can't find the resource that you asked for. - raise e - else: - # Other issues - raise e - else: - return secret_ids - - -def get_secret_string( - client: boto3.SecretsManagerClient, secret_id: str -) -> str: - try: - get_secret_value_response = client.get_secret_value(SecretId=secret_id) - except ClientError as e: - code = e.response["Error"]["Code"] - if code == "DecryptionFailureException": - # Secrets Manager can't decrypt the protected secret text using - # the provided KMS key. - raise e - elif code == "InternalServiceErrorException": - # An error occurred on the server side. - raise e - elif code == "InvalidParameterException": - # You provided an invalid value for a parameter. - raise e - elif code == "InvalidRequestException": - # You provided a parameter value that is not valid for the current - # state of the resource. - raise e - elif code == "ResourceNotFoundException": - # We can't find the resource that you asked for. - raise e - else: - # Other issues - raise e - else: - if "SecretString" not in get_secret_value_response: - raise ValueError( - f"Secret {secret_id!r} does not contain a secret string." - ) - - return get_secret_value_response["SecretString"] - - -@click.command() -@click.option("-f", "filename", default="") -@click.argument("secret_id_prefix") -@click.argument("target_directory") -def main(filename: str, secret_id_prefix: str, target_directory: str) -> None: - region = os.environ.get("AWS_REGION", "us-east-2") - session = boto3.session.Session(region_name=region) - secretsmanager = session.client(service_name="secretsmanager") - secret_ids = list_secret_ids_by_prefix( - secretsmanager, secret_id_prefix=secret_id_prefix - ) - if not secret_ids: - raise click.ClickException( - f"No secrets with the prefix {secret_id_prefix!r} can be found in" - f" region {region!r}" - ) - - target_dir_path = pathlib.Path(target_directory) - os.makedirs(target_directory, exist_ok=True) - - if filename: - if len(secret_ids) > 1: - raise click.ClickException( - f"More than one secret ID matched {secret_id_prefix} thus" - f" a custom filename {filename} cannot be used." - ) - click.echo( - f"secret named {secret_id_prefix!r} in region {region!r} to be" - f" copied from AWS SecretsManager to {target_directory}/{filename}" - ) - else: - click.echo( - f"{len(secret_ids)} secrets with prefix {secret_id_prefix!r} in" - f" region {region!r} to copy from AWS SecretsManager to" - f" {target_directory}" - ) - - for secret_id in secret_ids: - try: - secret = get_secret_string(secretsmanager, secret_id) - except ClientError as e: - click.echo(f"Reading {secret_id} failed:", err=True) - click.echo(str(e), err=True) - continue - - if secret_id.startswith(APP_PREFIX): - secret_id = secret_id[len(APP_PREFIX) :] - if filename: - secret_id = filename - with open(target_dir_path / secret_id, "w") as target_file: - target_file.write(secret) - if secret.endswith("-----END OPENSSH PRIVATE KEY-----"): - target_file.write("\n") - - -if __name__ == "__main__": - main() diff --git a/server/containers/aptrepo/makeindex.py b/server/containers/aptrepo/makeindex.py deleted file mode 100755 index be03b5b2..00000000 --- a/server/containers/aptrepo/makeindex.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations -from typing import * -from typing_extensions import TypedDict - -import argparse -import fnmatch -import json -import pathlib -import re -import subprocess -import tarfile -import tempfile - - -slot_regexp = re.compile( - r"^(\w+(?:-[a-zA-Z]*)*?)" - r"(?:-(\d+(?:-(?:alpha|beta|rc)\d+)?(?:-dev\d+)?))?$", - re.A, -) - - -version_regexp = re.compile( - r"""^ - (?P[0-9]+(?:\.[0-9]+)*) - (?P
-        [-]?
-        (?P(a|b|c|rc|alpha|beta|pre|preview))
-        [\.]?
-        (?P[0-9]+)?
-    )?
-    (?P
-        [\.]?
-        (?Pdev)
-        [\.]?
-        (?P[0-9]+)?
-    )?
-    (?:\+(?P[a-z0-9]+(?:[\.][a-z0-9]+)*))?
-    $""",
-    re.X | re.A,
-)
-
-
-class Version(TypedDict):
-    major: int
-    minor: int
-    patch: int
-    prerelease: Tuple[str, ...]
-    metadata: Tuple[str, ...]
-
-
-def parse_version(ver: str) -> Version:
-    v = version_regexp.match(ver)
-    if v is None:
-        raise ValueError(f"cannot parse version: {ver}")
-    metadata = []
-    prerelease: List[str] = []
-    if v.group("pre"):
-        pre_l = v.group("pre_l")
-        if pre_l in {"a", "alpha"}:
-            pre_kind = "alpha"
-        elif pre_l in {"b", "beta"}:
-            pre_kind = "beta"
-        elif pre_l in {"c", "rc"}:
-            pre_kind = "rc"
-        else:
-            raise ValueError(f"cannot determine release stage from {ver}")
-
-        prerelease.append(f"{pre_kind}.{v.group('pre_n')}")
-        if v.group("dev"):
-            prerelease.append(f'dev.{v.group("dev_n")}')
-
-    elif v.group("dev"):
-        prerelease.append("alpha.1")
-        prerelease.append(f'dev.{v.group("dev_n")}')
-
-    if v.group("local"):
-        metadata.extend(v.group("local").split("."))
-
-    release = [int(r) for r in v.group("release").split(".")]
-
-    return Version(
-        major=release[0],
-        minor=release[1],
-        patch=release[2] if len(release) == 3 else 0,
-        prerelease=tuple(prerelease),
-        metadata=tuple(metadata),
-    )
-
-
-def format_version_key(ver: Version, revision: str) -> str:
-    ver_key = f'{ver["major"]}.{ver["minor"]}.{ver["patch"]}'
-    if ver["prerelease"]:
-        # Using tilde for "dev" makes it sort _before_ the equivalent
-        # version without "dev" when using the GNU version sort (sort -V)
-        # or debian version comparison algorithm.
-        prerelease = (
-            ("~" if pre.startswith("dev.") else ".") + pre
-            for pre in ver["prerelease"]
-        )
-        ver_key += "~" + "".join(prerelease).lstrip(".~")
-    if revision:
-        ver_key += f".{revision}"
-    return ver_key
-
-
-def extract_catver(path: str) -> Optional[int]:
-    cv_prefix = "EDGEDB_CATALOG_VERSION = "
-    defines_pattern = (
-        "*/usr/lib/*-linux-gnu/edgedb-server-*/lib/python*/site-packages/edb"
-        + "/server/defines.py"
-    )
-
-    with tempfile.TemporaryDirectory() as _td:
-        td = pathlib.Path(_td)
-        subprocess.run(["ar", "x", path, "data.tar.xz"], cwd=_td)
-        with tarfile.open(td / "data.tar.xz", "r:xz") as tarf:
-            for member in tarf.getmembers():
-                if fnmatch.fnmatch(member.path, defines_pattern):
-                    df = tarf.extractfile(member)
-                    if df is not None:
-                        for lb in df.readlines():
-                            line = lb.decode()
-                            if line.startswith(cv_prefix):
-                                return int(line[len(cv_prefix) :])
-
-    return None
-
-
-def main():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--full-regen", action="store_true")
-    parser.add_argument("repopath")
-    parser.add_argument("outputdir")
-    args = parser.parse_args()
-
-    result = subprocess.run(
-        ["reprepro", "-b", args.repopath, "dumpreferences"],
-        universal_newlines=True,
-        check=True,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.STDOUT,
-    )
-
-    dists = set()
-
-    for line in result.stdout.split("\n"):
-        if not line.strip():
-            continue
-
-        dist, _, _ = line.partition("|")
-        dists.add(dist)
-
-    list_format = (
-        r"\0".join(
-            (
-                r"${$architecture}",
-                r"${package}",
-                r"${version}",
-                r"${$fullfilename}",
-            )
-        )
-        + r"\n"
-    )
-
-    idxdir = pathlib.Path(args.outputdir)
-
-    for dist in dists:
-        idxfile = idxdir / f"{dist}.json"
-        existing = {}
-
-        if idxfile.exists():
-            with open(idxfile, "r") as f:
-                index = json.load(f)
-                if index and "packages" in index:
-                    for entry in index["packages"]:
-                        if "version_key" in entry:
-                            existing[
-                                entry["name"], entry["version_key"]
-                            ] = entry
-
-        result = subprocess.run(
-            [
-                "reprepro",
-                "-b",
-                args.repopath,
-                f"--list-format={list_format}",
-                "list",
-                dist,
-            ],
-            universal_newlines=True,
-            check=True,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.STDOUT,
-        )
-
-        index = []
-        for line in result.stdout.split("\n"):
-            if not line.strip():
-                continue
-
-            arch, pkgname, pkgver, pkgfile = line.split("\0")
-            relver, _, revver = pkgver.rpartition("-")
-
-            m = slot_regexp.match(pkgname)
-            if not m:
-                print("cannot parse package name: {}".format(pkgname))
-                basename = pkgname
-                slot = None
-            else:
-                basename = m.group(1)
-                slot = m.group(2)
-
-            if arch == "amd64":
-                arch = "x86_64"
-
-            parsed_ver = parse_version(relver)
-            version_key = format_version_key(parsed_ver, revver)
-
-            if (pkgname, version_key) in existing and not args.full_regen:
-                index.append(existing[pkgname, version_key])
-                continue
-
-            if (
-                not any(m.startswith("cv") for m in parsed_ver["metadata"])
-                and basename == "edgedb-server"
-            ):
-                if not pathlib.Path(pkgfile).exists():
-                    print(f"package file does not exist: {pkgfile}")
-                else:
-                    catver = extract_catver(pkgfile)
-                    if catver is None:
-                        print(f"cannot extract catalog version from {pkgfile}")
-                    else:
-                        parsed_ver["metadata"] += (f"cv{catver}",)
-                        print(f"extracted catver {catver} from {pkgfile}")
-
-            installref = "{}={}-{}".format(pkgname, relver, revver)
-
-            index.append(
-                {
-                    "basename": basename,
-                    "slot": slot,
-                    "name": pkgname,
-                    "version": relver,
-                    "parsed_version": parsed_ver,
-                    "version_key": version_key,
-                    "revision": revver,
-                    "architecture": arch,
-                    "installref": installref,
-                }
-            )
-
-            print("makeindex: noted {}".format(installref))
-
-        with open(idxfile, "w") as f:
-            json.dump({"packages": index}, f)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/server/containers/aptrepo/processincoming.sh b/server/containers/aptrepo/processincoming.sh
deleted file mode 100755
index 441a0f0a..00000000
--- a/server/containers/aptrepo/processincoming.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/bin/bash
-
-set -Eexuo pipefail
-shopt -s nullglob
-
-if [ "$#" -ne 1 ]; then
-    echo "Usage: $(basename $0) " >&2
-    exit 1
-fi
-
-changes=$1
-localdir="%%REPREPRO_BASE_DIR%%"
-basedir="s3://edgedb-packages/apt"
-
-local_dist="${localdir}/"
-shared_dist="${basedir}/"
-
-aws s3 sync --delete --exact-timestamps \
-    "${shared_dist}db/" "${local_dist}db/"
-
-aws s3 sync --delete --exact-timestamps \
-    "${shared_dist}dists/" "${local_dist}dists/"
-
-aws s3 sync --delete --exact-timestamps \
-    "${shared_dist}.jsonindexes/" "${local_dist}.jsonindexes/"
-
-aws s3 sync --delete \
-    "${shared_dist}pool/" "${local_dist}pool/"
-
-reprepro -v -v --waitforlock 100 processincoming main "${changes}"
-mkdir -p "${local_dist}/.jsonindexes/"
-makeindex.py "${local_dist}" "${local_dist}/.jsonindexes"
-aws s3 sync --delete \
-            --cache-control "no-store, no-cache, private, max-age=0" \
-            "${local_dist}db/" "${shared_dist}db/"
-aws s3 sync --delete \
-            --cache-control "no-store, no-cache, private, max-age=0" \
-            "${local_dist}dists/" "${shared_dist}dists/"
-aws s3 sync --delete \
-            --cache-control "no-store, no-cache, private, max-age=0" \
-            "${local_dist}.jsonindexes/" "${shared_dist}.jsonindexes/"
-aws s3 sync --delete \
-            --cache-control "public, no-transform, max-age=315360000" \
-            "${local_dist}pool/" "${shared_dist}pool/"
diff --git a/server/containers/aptrepo/visor.py b/server/containers/aptrepo/visor.py
deleted file mode 100755
index badeeabe..00000000
--- a/server/containers/aptrepo/visor.py
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-from typing import *
-
-import asyncio
-import configparser
-import ctypes
-import ctypes.util
-import datetime
-import shlex
-import signal
-import subprocess
-import sys
-
-FILTER_OUT = ["Did not receive identification string from"]
-PROCESSES = []
-
-
-def out(txt: str, *args: Any, **kwargs: Any) -> None:
-    """A print wrapper with filtering and a timestamp in the front."""
-    for filtered in FILTER_OUT:
-        if filtered in txt:
-            return
-
-    now = datetime.datetime.utcnow()
-    millis = f"{now.microsecond:0>6}"[:3]
-    ts = now.strftime(f"%H:%M:%S.{millis}")
-    print(ts, txt, *args, **kwargs)
-
-
-def ensure_dead_with_parent() -> None:
-    """A last resort measure to make sure this process dies with its parent."""
-    if not sys.platform.startswith("linux"):
-        return
-
-    PR_SET_PDEATHSIG = 1  # include/uapi/linux/prctl.h
-    libc = ctypes.CDLL(ctypes.util.find_library("c"))  # type: ignore
-    libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL)
-
-
-async def keep_printing(prefix: str, stream: asyncio.StreamReader) -> None:
-    """Print from the stream with a prefix."""
-    while True:
-        line_b = await stream.readline()
-        if not line_b:
-            break
-        out(f"{prefix}: {line_b.decode('utf8')}", end="")
-
-
-async def run(cmd: List[str], user: str = "") -> int:
-    """Run a command and stream its stdout and stderr."""
-    cmd_name, args = cmd[0], cmd[1:]
-    if user:
-        out("Starting", cmd, "with", user)
-    else:
-        out("Starting", cmd)
-    run_cmd = cmd_name
-    if user:
-        run_cmd = "gosu"
-        args.insert(0, cmd_name)
-        args.insert(0, user)
-    proc = await asyncio.create_subprocess_exec(
-        run_cmd,
-        *args,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-        preexec_fn=ensure_dead_with_parent,
-    )
-    assert proc.stdout
-    assert proc.stderr
-    PROCESSES.append(proc)
-    out("Running", cmd_name, "at", proc.pid)
-    await asyncio.wait(
-        [
-            keep_printing("O " + cmd_name, proc.stdout),
-            keep_printing("E " + cmd_name, proc.stderr),
-        ]
-    )
-    return await proc.wait()
-
-
-def stop_processes(quiet: bool = False) -> None:
-    if not quiet:
-        out("Received SIGINT or SIGTERM, shutting down...")
-    for proc in PROCESSES:
-        try:
-            proc.terminate()
-        except BaseException:
-            continue
-
-
-async def async_main() -> int:
-    loop = asyncio.get_event_loop()
-    loop.add_signal_handler(signal.SIGINT, stop_processes)
-    loop.add_signal_handler(signal.SIGTERM, stop_processes)
-
-    commands = []
-    cfg = configparser.ConfigParser()
-    cfg.read_file(sys.stdin)
-    for sect_name, sect in cfg.items():
-        if sect_name == cfg.default_section:
-            continue
-        cmd = shlex.split(sect["cmd"])
-        if "user" in sect:
-            commands.append(run(cmd, user=sect["user"]))
-        else:
-            commands.append(run(cmd))
-
-    return_code = 0
-    for coro in asyncio.as_completed(commands):
-        earliest_return_code = await coro
-        if earliest_return_code and return_code == 0:
-            stop_processes(quiet=True)
-            return_code = earliest_return_code
-            loop.call_later(5, sys.exit, return_code)  # time out
-
-    out("Done.")
-    return return_code
-
-
-if __name__ == "__main__":
-    sys.exit(asyncio.run(async_main()))
diff --git a/server/containers/genrepo/Dockerfile b/server/containers/genrepo/Dockerfile
deleted file mode 100644
index 0e62e75c..00000000
--- a/server/containers/genrepo/Dockerfile
+++ /dev/null
@@ -1,58 +0,0 @@
-FROM debian:bullseye
-
-ENV REPO_LOCAL_DIR=/var/tmp/repo/local/
-ENV REPO_INCOMING_DIR=/var/spool/repo/incoming/
-
-RUN apt-get update \
-    && DEBIAN_FRONTEND=noninteractive \
-        apt-get install -y --no-install-recommends \
-            openssh-server bash inoticoming gosu gnupg libffi-dev \
-            gcc python3-dev python3-setuptools python3-pip python3-apt \
-            reprepro createrepo-c rpm dnf awscli \
-    && pip3 install boto3 \
-    && pip3 install mypy-boto3-s3 \
-    && pip3 install click \
-    && pip3 install typing-extensions \
-    && pip3 install tomli \
-    && pip3 install semver \
-    && addgroup --gid 2000 repomgr \
-    && adduser --uid 2000 --gid 2000 repomgr \
-    && addgroup uploaders \
-    && addgroup incoming \
-    && adduser --ingroup uploaders \
-               --home $REPO_INCOMING_DIR --no-create-home uploader \
-    && adduser repomgr incoming \
-    && adduser uploader incoming \
-    && mkdir -p $REPO_INCOMING_DIR/triggers \
-    && chown -R repomgr:incoming $REPO_INCOMING_DIR \
-    && chmod g+ws $REPO_INCOMING_DIR \
-    && chmod g+ws $REPO_INCOMING_DIR/triggers \
-    && mkdir -p /etc/ssh/authorized_keys/ \
-    && mkdir -p ~root/.ssh && chmod 700 ~root/.ssh/ \
-    && mkdir -p /etc/ssh.default/ \
-    && rm -rf /var/lib/apt/lists/*
-
-COPY config/genrepo.toml /etc/genrepo.toml
-COPY config/sshd/* /etc/ssh.default/
-COPY config/gnupg/* /home/repomgr/.gnupg/
-COPY config/rpm/rpmmacros /home/repomgr/.rpmmacros
-COPY entrypoint.sh /entrypoint.sh
-COPY fetch_secrets.py /usr/local/bin/
-COPY process_incoming.py /usr/local/bin/
-COPY visor.py /usr/local/bin/
-RUN sed -i -e "s|%%REPO_INCOMING_DIR%%|${REPO_INCOMING_DIR}|g" \
-        /usr/local/bin/process_incoming.py \
-    && sed -i -e "s|%%REPO_LOCAL_DIR%%|${REPO_LOCAL_DIR}|g" \
-        /usr/local/bin/process_incoming.py
-
-RUN chown -R repomgr:repomgr /home/repomgr/.gnupg \
-    && chmod 700 /home/repomgr/.gnupg \
-    && chmod 600 /home/repomgr/.gnupg/*
-
-EXPOSE 22
-
-VOLUME /var/tmp/repo
-
-ENTRYPOINT ["/entrypoint.sh"]
-
-CMD ["/usr/sbin/sshd", "-e", "-D", "-f", "/etc/ssh.default/sshd_config"]
diff --git a/server/containers/genrepo/config/gnupg/gpg-agent.conf b/server/containers/genrepo/config/gnupg/gpg-agent.conf
deleted file mode 100644
index e69de29b..00000000
diff --git a/server/containers/genrepo/config/gnupg/gpg.conf b/server/containers/genrepo/config/gnupg/gpg.conf
deleted file mode 100644
index e69de29b..00000000
diff --git a/server/containers/genrepo/config/rpm/rpmmacros b/server/containers/genrepo/config/rpm/rpmmacros
deleted file mode 100644
index 9064eb93..00000000
--- a/server/containers/genrepo/config/rpm/rpmmacros
+++ /dev/null
@@ -1,3 +0,0 @@
-%_signature gpg
-%__gpg /usr/bin/gpg
-%_gpg_name EdgeDB (release signing) 
diff --git a/server/containers/genrepo/config/sshd/sshd_config b/server/containers/genrepo/config/sshd/sshd_config
deleted file mode 100644
index 1fc796f3..00000000
--- a/server/containers/genrepo/config/sshd/sshd_config
+++ /dev/null
@@ -1,6 +0,0 @@
-StrictModes no
-PasswordAuthentication no
-Subsystem sftp /usr/lib/openssh/sftp-server
-AcceptEnv LANG LC_*
-PrintMotd no
-UsePAM yes
diff --git a/server/containers/genrepo/config/sshd/sshd_config_conditional b/server/containers/genrepo/config/sshd/sshd_config_conditional
deleted file mode 100644
index 97d3e757..00000000
--- a/server/containers/genrepo/config/sshd/sshd_config_conditional
+++ /dev/null
@@ -1,7 +0,0 @@
-Match Group uploaders
-        ChrootDirectory /var/spool/repo/
-        ForceCommand internal-sftp
-        AllowTcpForwarding no
-        PermitTunnel no
-        X11Forwarding no
-        AuthorizedKeysFile /etc/ssh/authorized_keys/uploaders
diff --git a/server/containers/genrepo/entrypoint.sh b/server/containers/genrepo/entrypoint.sh
deleted file mode 100755
index cec7f05a..00000000
--- a/server/containers/genrepo/entrypoint.sh
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-[ "$DEBUG" == 'true' ] && set -x
-
-mkdir -p "${REPO_LOCAL_DIR}"
-chown -R repomgr:repomgr "${REPO_LOCAL_DIR}"
-chmod -R g+ws "${REPO_LOCAL_DIR}"
-
-if [ -w ~/.ssh ]; then
-    chown root:root ~/.ssh && chmod 700 ~/.ssh/
-fi
-
-fetch_secrets.py client-keys-root /root/.ssh -f authorized_keys
-fetch_secrets.py client-keys-uploaders /etc/ssh/authorized_keys -f uploaders
-
-if [ -w ~/.ssh/authorized_keys ]; then
-    chown root:root ~/.ssh/authorized_keys
-    chmod 400 ~/.ssh/authorized_keys
-fi
-
-fetch_secrets.py server-host-key- /etc/ssh/
-
-if ls /etc/ssh/server-host-key-* 1> /dev/null 2>&1; then
-    echo "Found shared ssh host keys in /etc/ssh/"
-    SSH_KEY_WILDCARD="server-host-key-*"
-elif ls /etc/ssh/ssh_host_* 1> /dev/null 2>&1; then
-    echo "Found custom ssh host keys in /etc/ssh/"
-    SSH_KEY_WILDCARD="ssh_host_*_key"
-else
-    echo "No ssh host keys found in /etc/ssh.  Generating."
-    ssh-keygen -A
-    SSH_KEY_WILDCARD="ssh_host_*_key"
-fi
-
-while IFS= read -r -d '' path; do
-    echo HostKey "${path}" >> "/etc/ssh.default/sshd_config"
-    if [ -w "${path}" ]; then
-        chown root:root "${path}"
-        chmod 400 "${path}"
-    fi
-done < <(find "/etc/ssh/" -name $SSH_KEY_WILDCARD -print0)
-
-if [ "$DEBUG" == 'true' ]; then
-    echo "LogLevel DEBUG2" >> "/etc/ssh.default/sshd_config"
-fi
-
-fetch_secrets.py release-signing- /root/gpg-keys/
-
-if [ -e "/root/gpg-keys/" ]; then
-    while IFS= read -r -d '' path; do
-        cat "${path}" | gosu repomgr:repomgr gpg --import
-    done < <(find "/root/gpg-keys/" -maxdepth 1 -type f -print0)
-fi
-
-if [ "${AWS_ACCESS_KEY_ID}" != "" ]; then
-    mkdir -p /home/repomgr/.aws
-    echo "[default]" >/home/repomgr/.aws/credentials
-    echo "aws_access_key_id = ${AWS_ACCESS_KEY_ID}" \
-        >>/home/repomgr/.aws/credentials
-    echo "aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" \
-        >>/home/repomgr/.aws/credentials
-    chown -R repomgr:repomgr /home/repomgr/.aws
-    chmod 400 /home/repomgr/.aws/credentials
-fi
-
-if [ -n "${PORT}" ]; then
-    echo "Port ${PORT}" >> "/etc/ssh.default/sshd_config"
-else
-    echo "Port 22" >> "/etc/ssh.default/sshd_config"
-fi
-
-# Conditional blocks go last
-cat "/etc/ssh.default/sshd_config_conditional" >> \
-    "/etc/ssh.default/sshd_config"
-
-mkdir -p /var/run/sshd
-
-if [ "$(basename $1)" == "sshd" ]; then
-    if [ "$DEBUG" == 'true' ]; then
-        echo "sshd_config"
-        echo "-----------"
-        cat "/etc/ssh.default/sshd_config"
-    fi
-    export PYTHONUNBUFFERED=1
-    /usr/local/bin/visor.py << EOF
-        [sshd]
-        cmd = $@
-        [inoticoming]
-        user = repomgr
-        cmd =
-            inoticoming
-            --initialsearch
-            --foreground
-            ${REPO_INCOMING_DIR}/triggers/
-            /usr/local/bin/process_incoming.py
-            --incoming-dir=${REPO_INCOMING_DIR}
-            --local-dir=${REPO_LOCAL_DIR}
-            triggers/{}
-            \;
-EOF
-else
-    exec "$@"
-fi
diff --git a/server/containers/genrepo/fetch_secrets.py b/server/containers/genrepo/fetch_secrets.py
deleted file mode 100755
index 22e6e48a..00000000
--- a/server/containers/genrepo/fetch_secrets.py
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-from typing import *
-
-import functools
-import os
-import pathlib
-
-import boto3
-from botocore.exceptions import ClientError
-import click
-
-
-APP_PREFIX = "edbcloud/app/edgedbeng/pkg/"
-
-
-def list_secret_ids_by_prefix(
-    client: boto3.SecretsManagerClient,
-    secret_id_prefix: str,
-) -> Set[str]:
-    paginator = client.get_paginator("list_secrets")
-    secret_ids = set()
-
-    try:
-        for page in paginator.paginate(
-            Filters=[
-                {
-                    "Key": "name",
-                    "Values": [f"{APP_PREFIX}{secret_id_prefix}"],
-                },
-            ],
-        ):
-            for secret in page["SecretList"]:
-                secret_ids.add(secret["Name"])
-    except ClientError as e:
-        code = e.response["Error"]["Code"]
-        if code == "DecryptionFailureException":
-            # Secrets Manager can't decrypt the protected secret text using
-            # the provided KMS key.
-            raise e
-        elif code == "InternalServiceErrorException":
-            # An error occurred on the server side.
-            raise e
-        elif code == "InvalidParameterException":
-            # You provided an invalid value for a parameter.
-            raise e
-        elif code == "InvalidRequestException":
-            # You provided a parameter value that is not valid for the current
-            # state of the resource.
-            raise e
-        elif code == "ResourceNotFoundException":
-            # We can't find the resource that you asked for.
-            raise e
-        else:
-            # Other issues
-            raise e
-    else:
-        return secret_ids
-
-
-def get_secret_string(
-    client: boto3.SecretsManagerClient, secret_id: str
-) -> str:
-    try:
-        get_secret_value_response = client.get_secret_value(SecretId=secret_id)
-    except ClientError as e:
-        code = e.response["Error"]["Code"]
-        if code == "DecryptionFailureException":
-            # Secrets Manager can't decrypt the protected secret text using
-            # the provided KMS key.
-            raise e
-        elif code == "InternalServiceErrorException":
-            # An error occurred on the server side.
-            raise e
-        elif code == "InvalidParameterException":
-            # You provided an invalid value for a parameter.
-            raise e
-        elif code == "InvalidRequestException":
-            # You provided a parameter value that is not valid for the current
-            # state of the resource.
-            raise e
-        elif code == "ResourceNotFoundException":
-            # We can't find the resource that you asked for.
-            raise e
-        else:
-            # Other issues
-            raise e
-    else:
-        if "SecretString" not in get_secret_value_response:
-            raise ValueError(
-                f"Secret {secret_id!r} does not contain a secret string."
-            )
-
-        return get_secret_value_response["SecretString"]
-
-
-@click.command()
-@click.option("-f", "filename", default="")
-@click.argument("secret_id_prefix")
-@click.argument("target_directory")
-def main(filename: str, secret_id_prefix: str, target_directory: str) -> None:
-    region = os.environ.get("AWS_REGION", "us-east-2")
-    session = boto3.session.Session(region_name=region)
-    secretsmanager = session.client(service_name="secretsmanager")
-    secret_ids = list_secret_ids_by_prefix(
-        secretsmanager, secret_id_prefix=secret_id_prefix
-    )
-    if not secret_ids:
-        raise click.ClickException(
-            f"No secrets with the prefix {secret_id_prefix!r} can be found in"
-            f" region {region!r}"
-        )
-
-    target_dir_path = pathlib.Path(target_directory)
-    os.makedirs(target_directory, exist_ok=True)
-
-    if filename:
-        if len(secret_ids) > 1:
-            raise click.ClickException(
-                f"More than one secret ID matched {secret_id_prefix} thus"
-                f" a custom filename {filename} cannot be used."
-            )
-        click.echo(
-            f"secret named {secret_id_prefix!r} in region {region!r} to be"
-            f" copied from AWS SecretsManager to {target_directory}/{filename}"
-        )
-    else:
-        click.echo(
-            f"{len(secret_ids)} secrets with prefix {secret_id_prefix!r} in"
-            f" region {region!r} to copy from AWS SecretsManager to"
-            f" {target_directory}"
-        )
-
-    for secret_id in secret_ids:
-        try:
-            secret = get_secret_string(secretsmanager, secret_id)
-        except ClientError as e:
-            click.echo(f"Reading {secret_id} failed:", err=True)
-            click.echo(str(e), err=True)
-            continue
-
-        if secret_id.startswith(APP_PREFIX):
-            secret_id = secret_id[len(APP_PREFIX) :]
-        if filename:
-            secret_id = filename
-        with open(target_dir_path / secret_id, "w") as target_file:
-            target_file.write(secret)
-            if secret.endswith("-----END OPENSSH PRIVATE KEY-----"):
-                target_file.write("\n")
-
-
-if __name__ == "__main__":
-    main()
diff --git a/server/containers/genrepo/visor.py b/server/containers/genrepo/visor.py
deleted file mode 100755
index 15af1c45..00000000
--- a/server/containers/genrepo/visor.py
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-from typing import *
-
-import asyncio
-import configparser
-import ctypes
-import ctypes.util
-import datetime
-import shlex
-import signal
-import subprocess
-import sys
-
-FILTER_OUT = frozenset(
-    {
-        "Did not receive identification string from",
-        "Received disconnect from",
-        "Disconnected from invalid user",
-        "Invalid user",
-        "Authentication fail",
-        "Disconnected from authenticating user",
-        "Connection closed by",
-        "Connection reset by",
-    }
-)
-PROCESSES = []
-
-
-def out(txt: str, *args: Any, **kwargs: Any) -> None:
-    """A print wrapper with filtering and a timestamp in the front."""
-    for filtered in FILTER_OUT:
-        if filtered in txt:
-            return
-
-    now = datetime.datetime.utcnow()
-    millis = f"{now.microsecond:0>6}"[:3]
-    ts = now.strftime(f"%H:%M:%S.{millis}")
-    print(ts, txt, *args, **kwargs)
-
-
-def ensure_dead_with_parent() -> None:
-    """A last resort measure to make sure this process dies with its parent."""
-    if not sys.platform.startswith("linux"):
-        return
-
-    PR_SET_PDEATHSIG = 1  # include/uapi/linux/prctl.h
-    libc = ctypes.CDLL(ctypes.util.find_library("c"))
-    libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL)
-
-
-async def keep_printing(prefix: str, stream: asyncio.StreamReader) -> None:
-    """Print from the stream with a prefix."""
-    while True:
-        line_b = await stream.readline()
-        if not line_b:
-            break
-        out(f"{prefix}: {line_b.decode('utf8')}", end="")
-
-
-async def run(cmd: List[str], user: str = "") -> int:
-    """Run a command and stream its stdout and stderr."""
-    cmd_name, args = cmd[0], cmd[1:]
-    if user:
-        out("Starting", cmd, "with", user)
-    else:
-        out("Starting", cmd)
-    run_cmd = cmd_name
-    if user:
-        run_cmd = "gosu"
-        args.insert(0, cmd_name)
-        args.insert(0, user)
-    proc = await asyncio.create_subprocess_exec(
-        run_cmd,
-        *args,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-        preexec_fn=ensure_dead_with_parent,
-    )
-    assert proc.stdout
-    assert proc.stderr
-    PROCESSES.append(proc)
-    out("Running", cmd_name, "at", proc.pid)
-    await asyncio.gather(
-        keep_printing("O " + cmd_name, proc.stdout),
-        keep_printing("E " + cmd_name, proc.stderr),
-    )
-    return await proc.wait()
-
-
-def stop_processes(quiet: bool = False) -> None:
-    if not quiet:
-        out("Received SIGINT or SIGTERM, shutting down...")
-    for proc in PROCESSES:
-        try:
-            proc.terminate()
-        except BaseException:
-            continue
-
-
-async def async_main() -> int:
-    loop = asyncio.get_event_loop()
-    loop.add_signal_handler(signal.SIGINT, stop_processes)
-    loop.add_signal_handler(signal.SIGTERM, stop_processes)
-
-    commands = []
-    cfg = configparser.ConfigParser()
-    cfg.read_file(sys.stdin)
-    for sect_name, sect in cfg.items():
-        if sect_name == cfg.default_section:
-            continue
-        cmd = shlex.split(sect["cmd"])
-        if "user" in sect:
-            commands.append(run(cmd, user=sect["user"]))
-        else:
-            commands.append(run(cmd))
-
-    return_code = 0
-    for coro in asyncio.as_completed(commands):
-        earliest_return_code = await coro
-        if earliest_return_code and return_code == 0:
-            stop_processes(quiet=True)
-            return_code = earliest_return_code
-            loop.call_later(5, sys.exit, return_code)  # time out
-
-    out("Done.")
-    return return_code
-
-
-if __name__ == "__main__":
-    sys.exit(asyncio.run(async_main()))
diff --git a/server/containers/rpmrepo/Dockerfile b/server/containers/rpmrepo/Dockerfile
deleted file mode 100644
index 9a2b3f30..00000000
--- a/server/containers/rpmrepo/Dockerfile
+++ /dev/null
@@ -1,61 +0,0 @@
-FROM centos:8
-
-ENV REPO_LOCAL_DIR=/var/tmp/repo/local/
-ENV REPO_INCOMING_DIR=/var/spool/repo/incoming/
-
-RUN yum install -y \
-        https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm \
-    && yum install -y \
-        createrepo openssh-server bash gnupg rpm-sign wget \
-        yum-utils gcc gcc-c++ make python38 python38-devel libffi-devel \
-    && pip3.8 install boto3 awscli click typing-extensions \
-    && groupadd --gid 2000 repomgr \
-    && useradd --uid 2000 --gid 2000 repomgr \
-    && groupadd uploaders \
-    && groupadd incoming \
-    && useradd -G uploaders -d $REPO_INCOMING_DIR --no-create-home uploader \
-    && gpasswd -a repomgr incoming \
-    && gpasswd -a uploader incoming \
-    && mkdir -p $REPO_INCOMING_DIR/triggers \
-    && chown -R repomgr:incoming $REPO_INCOMING_DIR \
-    && chmod -R g+ws $REPO_INCOMING_DIR \
-    && mkdir -p ~root/.ssh && chmod 700 ~root/.ssh/ \
-    && mkdir -p /etc/ssh.default/
-
-RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
-    && chmod +x /usr/local/bin/gosu \
-    && gosu --version \
-    && cd /tmp/ \
-    && wget https://salsa.debian.org/brlink/inoticoming/-/archive/debian/inoticoming-debian.tar.gz \
-    && tar xzvf inoticoming-debian.tar.gz \
-    && cd /tmp/inoticoming-debian \
-    && ./configure && make install \
-    && cd /tmp/ \
-    && rm -rf inoticoming-debian inoticoming-debian.tar.gz \
-    && inoticoming --version
-
-COPY config/sshd/* /etc/ssh.default/
-COPY config/gnupg/* /home/repomgr/.gnupg/
-COPY config/rpm/rpmmacros /home/repomgr/.rpmmacros
-COPY entrypoint.sh /entrypoint.sh
-COPY fetch_secrets.py /usr/local/bin/
-COPY remove_old_dev_pkg.py /usr/local/bin/
-COPY processincoming.sh /usr/local/bin/
-COPY makeindex.py /usr/local/bin/
-COPY visor.py /usr/local/bin/
-RUN sed -i -e "s|%%REPO_INCOMING_DIR%%|${REPO_INCOMING_DIR}|g" \
-        /usr/local/bin/processincoming.sh \
-    && sed -i -e "s|%%REPO_LOCAL_DIR%%|${REPO_LOCAL_DIR}|g" \
-        /usr/local/bin/processincoming.sh
-
-RUN chown -R repomgr:repomgr /home/repomgr/.gnupg \
-    && chmod 700 /home/repomgr/.gnupg \
-    && chmod 600 /home/repomgr/.gnupg/*
-
-EXPOSE 22
-
-VOLUME /var/tmp/repo
-
-ENTRYPOINT ["/entrypoint.sh"]
-
-CMD ["/usr/sbin/sshd", "-e", "-D", "-f", "/etc/ssh.default/sshd_config"]
diff --git a/server/containers/rpmrepo/config/gnupg/gpg-agent.conf b/server/containers/rpmrepo/config/gnupg/gpg-agent.conf
deleted file mode 100644
index e69de29b..00000000
diff --git a/server/containers/rpmrepo/config/gnupg/gpg.conf b/server/containers/rpmrepo/config/gnupg/gpg.conf
deleted file mode 100644
index e69de29b..00000000
diff --git a/server/containers/rpmrepo/config/rpm/rpmmacros b/server/containers/rpmrepo/config/rpm/rpmmacros
deleted file mode 100644
index 424ea439..00000000
--- a/server/containers/rpmrepo/config/rpm/rpmmacros
+++ /dev/null
@@ -1,2 +0,0 @@
-%_signature gpg
-%_gpg_name EdgeDB (release signing) 
diff --git a/server/containers/rpmrepo/config/sshd/sshd_config b/server/containers/rpmrepo/config/sshd/sshd_config
deleted file mode 100644
index e84ae767..00000000
--- a/server/containers/rpmrepo/config/sshd/sshd_config
+++ /dev/null
@@ -1,6 +0,0 @@
-StrictModes no
-PasswordAuthentication no
-Subsystem sftp internal-sftp -u 0002
-AcceptEnv LANG LC_*
-PrintMotd no
-UsePAM yes
diff --git a/server/containers/rpmrepo/config/sshd/sshd_config_conditional b/server/containers/rpmrepo/config/sshd/sshd_config_conditional
deleted file mode 100644
index 2d178e46..00000000
--- a/server/containers/rpmrepo/config/sshd/sshd_config_conditional
+++ /dev/null
@@ -1,7 +0,0 @@
-Match Group uploaders
-        ChrootDirectory /var/spool/repo/
-        ForceCommand internal-sftp -u 0002
-        AllowTcpForwarding no
-        PermitTunnel no
-        X11Forwarding no
-        AuthorizedKeysFile /etc/ssh/authorized_keys/uploaders
diff --git a/server/containers/rpmrepo/entrypoint.sh b/server/containers/rpmrepo/entrypoint.sh
deleted file mode 100755
index af2c2dbd..00000000
--- a/server/containers/rpmrepo/entrypoint.sh
+++ /dev/null
@@ -1,109 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-[ "$DEBUG" == 'true' ] && set -x
-
-mkdir -p "${REPO_LOCAL_DIR}"
-chown -R repomgr:repomgr "${REPO_LOCAL_DIR}"
-chmod -R g+ws "${REPO_LOCAL_DIR}"
-
-if [ -w ~/.ssh ]; then
-    chown root:root ~/.ssh && chmod 700 ~/.ssh/
-fi
-
-fetch_secrets.py client-keys-root /root/.ssh -f authorized_keys
-fetch_secrets.py client-keys-uploaders /etc/ssh/authorized_keys -f uploaders
-
-if [ -w ~/.ssh/authorized_keys ]; then
-    chown root:root ~/.ssh/authorized_keys
-    chmod 400 ~/.ssh/authorized_keys
-fi
-
-fetch_secrets.py server-host-key- /etc/ssh/
-
-if ls /etc/ssh/server-host-key-* 1> /dev/null 2>&1; then
-    echo "Found shared ssh host keys in /etc/ssh/"
-    SSH_KEY_WILDCARD="server-host-key-*"
-elif ls /etc/ssh/ssh_host_* 1> /dev/null 2>&1; then
-    echo "Found custom ssh host keys in /etc/ssh/"
-    SSH_KEY_WILDCARD="ssh_host_*_key"
-else
-    echo "No ssh host keys found in /etc/ssh.  Generating."
-    ssh-keygen -A
-    SSH_KEY_WILDCARD="ssh_host_*_key"
-fi
-
-find /etc/ssh -name $SSH_KEY_WILDCARD
-
-while IFS= read -r -d '' path; do
-    echo HostKey "${path}" >> "/etc/ssh.default/sshd_config"
-    if [ -w "${path}" ]; then
-        chown root:root "${path}"
-        chmod 400 "${path}"
-    fi
-done < <(find "/etc/ssh/" -name $SSH_KEY_WILDCARD -print0)
-
-if [ "$DEBUG" == 'true' ]; then
-    echo "LogLevel DEBUG2" >> "/etc/ssh.default/sshd_config"
-fi
-
-fetch_secrets.py release-signing- /root/gpg-keys/
-
-if [ -e "/root/gpg-keys/" ]; then
-    while IFS= read -r -d '' path; do
-        cat "${path}" | gosu repomgr:repomgr gpg --import
-    done < <(find "/root/gpg-keys/" -maxdepth 1 -type f -print0)
-fi
-
-if [ "${AWS_ACCESS_KEY_ID}" != "" ]; then
-    mkdir -p /home/repomgr/.aws
-    echo "[default]" >/home/repomgr/.aws/credentials
-    echo "aws_access_key_id = ${AWS_ACCESS_KEY_ID}" >>/home/repomgr/.aws/credentials
-    echo "aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" >>/home/repomgr/.aws/credentials
-    chown -R repomgr:repomgr /home/repomgr/.aws
-    chmod 400 /home/repomgr/.aws/credentials
-fi
-
-gosu repomgr:repomgr aws s3 sync --delete --exact-timestamps \
-    s3://edgedb-packages/rpm/ "${REPO_LOCAL_DIR}/"
-
-if [ -n "${PORT}" ]; then
-    echo "Port ${PORT}" >> "/etc/ssh.default/sshd_config"
-else
-    echo "Port 22" >> "/etc/ssh.default/sshd_config"
-fi
-
-# Conditional blocks go last
-cat "/etc/ssh.default/sshd_config_conditional" >> \
-    "/etc/ssh.default/sshd_config"
-
-mkdir -p /var/run/sshd
-
-if [ "$(basename $1)" == "sshd" ]; then
-    if [ "$DEBUG" == 'true' ]; then
-        echo "sshd_config"
-        echo "-----------"
-        cat "/etc/ssh.default/sshd_config"
-    fi
-    rm /run/nologin
-    export PYTHONUNBUFFERED=1
-    /usr/local/bin/visor.py << EOF
-        [sshd]
-        cmd = $@
-        [inoticoming]
-        user = repomgr
-        cmd =
-            inoticoming
-            --initialsearch
-            --foreground
-            ${REPO_INCOMING_DIR}/triggers/
-            --chdir
-            ${REPO_INCOMING_DIR}
-            /usr/local/bin/processincoming.sh
-            triggers/{}
-            \;
-EOF
-else
-    exec "$@"
-fi
diff --git a/server/containers/rpmrepo/fetch_secrets.py b/server/containers/rpmrepo/fetch_secrets.py
deleted file mode 100755
index 22e6e48a..00000000
--- a/server/containers/rpmrepo/fetch_secrets.py
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-from typing import *
-
-import functools
-import os
-import pathlib
-
-import boto3
-from botocore.exceptions import ClientError
-import click
-
-
-APP_PREFIX = "edbcloud/app/edgedbeng/pkg/"
-
-
-def list_secret_ids_by_prefix(
-    client: boto3.SecretsManagerClient,
-    secret_id_prefix: str,
-) -> Set[str]:
-    paginator = client.get_paginator("list_secrets")
-    secret_ids = set()
-
-    try:
-        for page in paginator.paginate(
-            Filters=[
-                {
-                    "Key": "name",
-                    "Values": [f"{APP_PREFIX}{secret_id_prefix}"],
-                },
-            ],
-        ):
-            for secret in page["SecretList"]:
-                secret_ids.add(secret["Name"])
-    except ClientError as e:
-        code = e.response["Error"]["Code"]
-        if code == "DecryptionFailureException":
-            # Secrets Manager can't decrypt the protected secret text using
-            # the provided KMS key.
-            raise e
-        elif code == "InternalServiceErrorException":
-            # An error occurred on the server side.
-            raise e
-        elif code == "InvalidParameterException":
-            # You provided an invalid value for a parameter.
-            raise e
-        elif code == "InvalidRequestException":
-            # You provided a parameter value that is not valid for the current
-            # state of the resource.
-            raise e
-        elif code == "ResourceNotFoundException":
-            # We can't find the resource that you asked for.
-            raise e
-        else:
-            # Other issues
-            raise e
-    else:
-        return secret_ids
-
-
-def get_secret_string(
-    client: boto3.SecretsManagerClient, secret_id: str
-) -> str:
-    try:
-        get_secret_value_response = client.get_secret_value(SecretId=secret_id)
-    except ClientError as e:
-        code = e.response["Error"]["Code"]
-        if code == "DecryptionFailureException":
-            # Secrets Manager can't decrypt the protected secret text using
-            # the provided KMS key.
-            raise e
-        elif code == "InternalServiceErrorException":
-            # An error occurred on the server side.
-            raise e
-        elif code == "InvalidParameterException":
-            # You provided an invalid value for a parameter.
-            raise e
-        elif code == "InvalidRequestException":
-            # You provided a parameter value that is not valid for the current
-            # state of the resource.
-            raise e
-        elif code == "ResourceNotFoundException":
-            # We can't find the resource that you asked for.
-            raise e
-        else:
-            # Other issues
-            raise e
-    else:
-        if "SecretString" not in get_secret_value_response:
-            raise ValueError(
-                f"Secret {secret_id!r} does not contain a secret string."
-            )
-
-        return get_secret_value_response["SecretString"]
-
-
-@click.command()
-@click.option("-f", "filename", default="")
-@click.argument("secret_id_prefix")
-@click.argument("target_directory")
-def main(filename: str, secret_id_prefix: str, target_directory: str) -> None:
-    region = os.environ.get("AWS_REGION", "us-east-2")
-    session = boto3.session.Session(region_name=region)
-    secretsmanager = session.client(service_name="secretsmanager")
-    secret_ids = list_secret_ids_by_prefix(
-        secretsmanager, secret_id_prefix=secret_id_prefix
-    )
-    if not secret_ids:
-        raise click.ClickException(
-            f"No secrets with the prefix {secret_id_prefix!r} can be found in"
-            f" region {region!r}"
-        )
-
-    target_dir_path = pathlib.Path(target_directory)
-    os.makedirs(target_directory, exist_ok=True)
-
-    if filename:
-        if len(secret_ids) > 1:
-            raise click.ClickException(
-                f"More than one secret ID matched {secret_id_prefix} thus"
-                f" a custom filename {filename} cannot be used."
-            )
-        click.echo(
-            f"secret named {secret_id_prefix!r} in region {region!r} to be"
-            f" copied from AWS SecretsManager to {target_directory}/{filename}"
-        )
-    else:
-        click.echo(
-            f"{len(secret_ids)} secrets with prefix {secret_id_prefix!r} in"
-            f" region {region!r} to copy from AWS SecretsManager to"
-            f" {target_directory}"
-        )
-
-    for secret_id in secret_ids:
-        try:
-            secret = get_secret_string(secretsmanager, secret_id)
-        except ClientError as e:
-            click.echo(f"Reading {secret_id} failed:", err=True)
-            click.echo(str(e), err=True)
-            continue
-
-        if secret_id.startswith(APP_PREFIX):
-            secret_id = secret_id[len(APP_PREFIX) :]
-        if filename:
-            secret_id = filename
-        with open(target_dir_path / secret_id, "w") as target_file:
-            target_file.write(secret)
-            if secret.endswith("-----END OPENSSH PRIVATE KEY-----"):
-                target_file.write("\n")
-
-
-if __name__ == "__main__":
-    main()
diff --git a/server/containers/rpmrepo/makeindex.py b/server/containers/rpmrepo/makeindex.py
deleted file mode 100755
index 1382a827..00000000
--- a/server/containers/rpmrepo/makeindex.py
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/usr/bin/env python3.8
-from __future__ import annotations
-from typing import *
-from typing_extensions import TypedDict
-
-import argparse
-import json
-import os.path
-import re
-import subprocess
-
-
-slot_regexp = re.compile(
-    r"^(\w+(?:-[a-zA-Z]*)*?)"
-    r"(?:-(\d+(?:-(?:alpha|beta|rc)\d+)?(?:-dev\d+)?))?$",
-    re.A,
-)
-
-
-version_regexp = re.compile(
-    r"""^
-    (?P[0-9]+(?:\.[0-9]+)*)
-    (?P
-        [-_]?
-        (?P(a|b|c|rc|alpha|beta|pre|preview))
-        [\.]?
-        (?P[0-9]+)?
-    )?
-    (?P
-        [\.]?
-        (?Pdev)
-        [\.]?
-        (?P[0-9]+)?
-    )?
-    (?:\+(?P[a-z0-9]+(?:[\.][a-z0-9]+)*))?
-    $""",
-    re.X | re.A,
-)
-
-
-class Version(TypedDict):
-    major: int
-    minor: int
-    patch: int
-    prerelease: Tuple[str, ...]
-    metadata: Tuple[str, ...]
-
-
-def parse_version(ver: str) -> Version:
-    v = version_regexp.match(ver)
-    if v is None:
-        raise ValueError(f"cannot parse version: {ver}")
-    metadata = []
-    prerelease: List[str] = []
-    if v.group("pre"):
-        pre_l = v.group("pre_l")
-        if pre_l in {"a", "alpha"}:
-            pre_kind = "alpha"
-        elif pre_l in {"b", "beta"}:
-            pre_kind = "beta"
-        elif pre_l in {"c", "rc"}:
-            pre_kind = "rc"
-        else:
-            raise ValueError(f"cannot determine release stage from {ver}")
-
-        prerelease.append(f"{pre_kind}.{v.group('pre_n')}")
-        if v.group("dev"):
-            prerelease.append(f'dev.{v.group("dev_n")}')
-
-    elif v.group("dev"):
-        prerelease.append("alpha.1")
-        prerelease.append(f'dev.{v.group("dev_n")}')
-
-    if v.group("local"):
-        metadata.extend(v.group("local").split("."))
-
-    release = [int(r) for r in v.group("release").split(".")]
-
-    return Version(
-        major=release[0],
-        minor=release[1],
-        patch=release[2] if len(release) == 3 else 0,
-        prerelease=tuple(prerelease),
-        metadata=tuple(metadata),
-    )
-
-
-def format_version_key(ver: Version, revision: str) -> str:
-    ver_key = f'{ver["major"]}.{ver["minor"]}.{ver["patch"]}'
-    if ver["prerelease"]:
-        # Using tilde for "dev" makes it sort _before_ the equivalent
-        # version without "dev" when using the GNU version sort (sort -V)
-        # or debian version comparison algorithm.
-        prerelease = (
-            ("~" if pre.startswith("dev.") else ".") + pre
-            for pre in ver["prerelease"]
-        )
-        ver_key += "~" + "".join(prerelease).lstrip(".~")
-    if revision:
-        ver_key += f".{revision}"
-    return ver_key
-
-
-def main():
-    parser = argparse.ArgumentParser()
-    parser.add_argument("repopath")
-    parser.add_argument("outputdir")
-    parser.add_argument("repoids")
-    args = parser.parse_args()
-
-    for dist in args.repoids.split(","):
-        result = subprocess.run(
-            [
-                "repoquery",
-                "--repofrompath={rid},{path}".format(
-                    rid=dist,
-                    path=os.path.join(args.repopath, dist),
-                ),
-                "--repoid={}".format(dist),
-                "--qf=%{name}|%{version}|%{release}|%{arch}",
-                "-q",
-                "*",
-            ],
-            universal_newlines=True,
-            check=True,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.STDOUT,
-        )
-
-        index = []
-
-        for line in result.stdout.split("\n"):
-            if not line.strip():
-                continue
-
-            pkgname, pkgver, release, arch = line.split("|")
-
-            m = slot_regexp.match(pkgname)
-            if not m:
-                print("cannot parse package name: {}".format(pkgname))
-                basename = pkgname
-                slot = None
-            else:
-                basename = m.group(1)
-                slot = m.group(2)
-
-            parsed_ver = parse_version(pkgver)
-
-            installref = "{}-{}-{}.{}".format(pkgname, pkgver, release, arch)
-            index.append(
-                {
-                    "basename": basename,
-                    "slot": slot,
-                    "name": pkgname,
-                    "version": pkgver,
-                    "parsed_version": parsed_ver,
-                    "version_key": format_version_key(parsed_ver, release),
-                    "revision": release,
-                    "architecture": arch,
-                    "installref": installref,
-                }
-            )
-
-            print("makeindex: noted {}".format(installref))
-
-        out = os.path.join(args.outputdir, "{}.json".format(dist))
-        with open(out, "w") as f:
-            json.dump({"packages": index}, f)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/server/containers/rpmrepo/processincoming.sh b/server/containers/rpmrepo/processincoming.sh
deleted file mode 100755
index e1e01898..00000000
--- a/server/containers/rpmrepo/processincoming.sh
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/bin/bash
-
-set -Eexuo pipefail
-shopt -s nullglob
-
-if [ "$#" -ne 1 ]; then
-    echo "Usage: $(basename $0) " >&2
-    exit 1
-fi
-
-list=$1
-incomingdir="%%REPO_INCOMING_DIR%%"
-localdir="%%REPO_LOCAL_DIR%%"
-basedir="s3://edgedb-packages/rpm"
-declare -A dists
-
-while read -r -u 10 pkgname; do
-
-    pkg="${incomingdir}/${pkgname}"
-    release=$(rpm -qp --queryformat '%{RELEASE}' "${pkg}")
-    dist=${release##*.}
-    releaseno=${release%.*}
-    subdist=$(echo ${releaseno} | sed 's/[[:digit:]]\+//')
-    if [ -n "${subdist}" ]; then
-        dist="${dist}.${subdist}"
-    fi
-
-    case "${dist}" in
-        el7*)
-            ;;
-        el8*)
-            ;;
-        *)
-            echo "Unsupported dist: ${dist}" >&2; exit 1 ;;
-    esac
-
-    local_dist="${localdir}/${dist}"
-    shared_dist="${basedir}/${dist}"
-    seendist=${dists[${dist}]+"${dists[${dist}]}"}
-
-    if [ -z "${seendist}" ]; then
-        dists["${dist}"]="true"
-        mkdir -p "${local_dist}"
-        aws s3 sync --delete --exact-timestamps \
-            "${shared_dist}/" "${local_dist}/"
-    fi
-
-    if [ ! -e "${local_dist}/repodata/repomd.xml" ]; then
-        createrepo --database "${local_dist}"
-    fi
-
-    mkdir -p /tmp/repo-staging/
-    cp "${pkg}" /tmp/repo-staging
-    rm -f "${pkg}"
-    echo | rpm --resign "/tmp/repo-staging/${pkgname}"
-    mv "/tmp/repo-staging/${pkgname}" "${local_dist}"
-
-    if [ "${subdist}" = "nightly" ]; then
-        old_rpms=$(repomanage --keep=3 --old "${local_dist}")
-        if [ -n "${old_rpms}" ]; then
-            rm "${old_rpms}"
-        fi
-        remove_old_dev_pkg.py --keep=3 ${local_dist}
-    fi
-
-    createrepo --update "${local_dist}"
-    gpg --yes --batch --detach-sign --armor "${local_dist}/repodata/repomd.xml"
-
-done 10<"${list}"
-
-
-for dist in "${!dists[@]}"; do
-    local_dist="${localdir}/${dist}"
-    shared_dist="${basedir}/${dist}"
-
-    mkdir -p "${localdir}/.jsonindexes/"
-    makeindex.py "${localdir}" "${localdir}/.jsonindexes/" "${dist}"
-
-    aws s3 sync --delete \
-                --cache-control "public, no-transform, max-age=315360000" \
-                --exclude "*" \
-                --include "*.rpm" \
-                ${local_dist}/ ${shared_dist}/
-    aws s3 sync --delete \
-                --cache-control "no-store, no-cache, private, max-age=0" \
-                ${local_dist}/repodata/ ${shared_dist}/repodata/
-    aws s3 sync --delete \
-                --cache-control "no-store, no-cache, private, max-age=0" \
-                ${localdir}/.jsonindexes/ ${basedir}/.jsonindexes/
-done
diff --git a/server/containers/rpmrepo/remove_old_dev_pkg.py b/server/containers/rpmrepo/remove_old_dev_pkg.py
deleted file mode 100755
index d7a3c6ff..00000000
--- a/server/containers/rpmrepo/remove_old_dev_pkg.py
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env python3
-from __future__ import annotations
-from typing import *
-
-import distutils.version
-import os
-import re
-
-import click
-
-
-# edgedb-server-1-alpha7-dev5124-1.0a7.dev5124+ged4e05af-2020101400nightly.el8.x86_64.rpm
-PACKAGE_RE = re.compile(
-    r"^(?P\w+(-[a-zA-Z]+)*)"
-    r"(?P-\d+(-(alpha|beta|rc)\d+)?(-dev\d+)?)?"
-    r"-(?P[^-]*)-(?P[^.]*)"
-    r"(?P.*)?$",
-    re.A,
-)
-PACKAGE_NAME_NO_DEV_RE = re.compile(r"([^-]+)((-[^-]+)*)-dev\d+")
-
-
-@click.command()
-@click.option("--keep", type=int, default=3)
-@click.argument("path")
-def main(path: str, keep: int) -> None:
-    index: Dict[str, List[Tuple[str, str]]] = {}
-    for file in os.scandir(path):
-        m = PACKAGE_RE.match(file.name)
-        if not m:
-            print(file.name, "doesn't match PACKAGE_RE")
-            continue
-
-        key_with_dev = f"{m.group('basename')}{m.group('slot') or ''}"
-        key = PACKAGE_NAME_NO_DEV_RE.sub(r"\1\2", key_with_dev)
-
-        version = f"{m.group('version')}_{m.group('release')}"
-        index.setdefault(key, []).append((version, file.name))
-
-    for _, versions in index.items():
-        sorted_versions = list(
-            sorted(
-                versions,
-                key=lambda v: distutils.version.LooseVersion(v[0]),
-                reverse=True,
-            )
-        )
-
-        for _ver, filename in sorted_versions[keep:]:
-            print("Deleting outdated", filename)
-            os.unlink(os.path.join(path, filename))
-
-
-if __name__ == "__main__":
-    main()
diff --git a/server/containers/rpmrepo/visor.py b/server/containers/rpmrepo/visor.py
deleted file mode 100755
index 35105893..00000000
--- a/server/containers/rpmrepo/visor.py
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env python3.8
-from __future__ import annotations
-from typing import *
-
-import asyncio
-import configparser
-import ctypes
-import ctypes.util
-import datetime
-import shlex
-import signal
-import subprocess
-import sys
-
-FILTER_OUT = [
-    "Did not receive identification string from",
-    "kex_exchange_identification: Connection closed by remote host",
-    "kex_exchange_identification: read: Connection reset by peer",
-]
-PROCESSES = []
-
-
-def out(txt: str, *args: Any, **kwargs: Any) -> None:
-    """A print wrapper with filtering and a timestamp in the front."""
-    for filtered in FILTER_OUT:
-        if filtered in txt:
-            return
-
-    now = datetime.datetime.utcnow()
-    millis = f"{now.microsecond:0>6}"[:3]
-    ts = now.strftime(f"%H:%M:%S.{millis}")
-    print(ts, txt, *args, **kwargs)
-
-
-def ensure_dead_with_parent() -> None:
-    """A last resort measure to make sure this process dies with its parent."""
-    if not sys.platform.startswith("linux"):
-        return
-
-    PR_SET_PDEATHSIG = 1  # include/uapi/linux/prctl.h
-    libc = ctypes.CDLL(ctypes.util.find_library("c"))  # type: ignore
-    libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL)
-
-
-async def keep_printing(prefix: str, stream: asyncio.StreamReader) -> None:
-    """Print from the stream with a prefix."""
-    while True:
-        line_b = await stream.readline()
-        if not line_b:
-            break
-        out(f"{prefix}: {line_b.decode('utf8')}", end="")
-
-
-async def run(cmd: List[str], user: str = "") -> int:
-    """Run a command and stream its stdout and stderr."""
-    cmd_name, args = cmd[0], cmd[1:]
-    if user:
-        out("Starting", cmd, "with", user)
-    else:
-        out("Starting", cmd)
-    run_cmd = cmd_name
-    if user:
-        run_cmd = "gosu"
-        args.insert(0, cmd_name)
-        args.insert(0, user)
-    proc = await asyncio.create_subprocess_exec(
-        run_cmd,
-        *args,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-        preexec_fn=ensure_dead_with_parent,
-    )
-    assert proc.stdout
-    assert proc.stderr
-    PROCESSES.append(proc)
-    out("Running", cmd_name, "at", proc.pid)
-    await asyncio.wait(
-        [
-            keep_printing("O " + cmd_name, proc.stdout),
-            keep_printing("E " + cmd_name, proc.stderr),
-        ]
-    )
-    return await proc.wait()
-
-
-def stop_processes(quiet: bool = False) -> None:
-    if not quiet:
-        out("Received SIGINT or SIGTERM, shutting down...")
-    for proc in PROCESSES:
-        try:
-            proc.terminate()
-        except BaseException:
-            continue
-
-
-async def async_main() -> int:
-    loop = asyncio.get_event_loop()
-    loop.add_signal_handler(signal.SIGINT, stop_processes)
-    loop.add_signal_handler(signal.SIGTERM, stop_processes)
-
-    commands = []
-    cfg = configparser.ConfigParser()
-    cfg.read_file(sys.stdin)
-    for sect_name, sect in cfg.items():
-        if sect_name == cfg.default_section:
-            continue
-        cmd = shlex.split(sect["cmd"])
-        if "user" in sect:
-            commands.append(run(cmd, user=sect["user"]))
-        else:
-            commands.append(run(cmd))
-
-    return_code = 0
-    for coro in asyncio.as_completed(commands):
-        earliest_return_code = await coro
-        if earliest_return_code and return_code == 0:
-            stop_processes(quiet=True)
-            return_code = earliest_return_code
-            loop.call_later(5, sys.exit, return_code)  # time out
-
-    out("Done.")
-    return return_code
-
-
-if __name__ == "__main__":
-    sys.exit(asyncio.run(async_main()))
diff --git a/server/containers/genrepo/config/genrepo.toml b/server/genrepo.toml
similarity index 100%
rename from server/containers/genrepo/config/genrepo.toml
rename to server/genrepo.toml
diff --git a/server/containers/genrepo/process_incoming.py b/server/process_incoming.py
similarity index 100%
rename from server/containers/genrepo/process_incoming.py
rename to server/process_incoming.py
diff --git a/server/pyproject.toml b/server/pyproject.toml
new file mode 100644
index 00000000..be52d73b
--- /dev/null
+++ b/server/pyproject.toml
@@ -0,0 +1,28 @@
+[project]
+name = "process_incoming"
+description = "Universal package processor"
+authors = [{ name = "EdgeDB Inc", email = "hello@magic.io" }]
+requires-python = '>=3.8.0'
+readme = "README.rst"
+version = "1.0.0"
+dependencies = [
+    'boto3',
+    'mypy-boto3-s3',
+    'click',
+    'typing-extensions',
+    'tomli',
+    'semver',
+]
+
+[project.urls]
+github = "https://github.com/edgedb/edgedb-pkg/"
+
+[project.scripts]
+process_incoming = "process_incoming:main"
+
+[build-system]
+requires = ["setuptools>=59", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools]
+py-modules = ["process_incoming"]