diff --git a/bin/split-silo-database b/bin/split-silo-database index e34ca250a96920..d65710b0fa2ec3 100755 --- a/bin/split-silo-database +++ b/bin/split-silo-database @@ -1,11 +1,11 @@ #!/usr/bin/env python -import os + +import subprocess import click from django.apps import apps from sentry.runner import configure -from sentry.runner.commands.devservices import get_docker_client from sentry.silo.base import SiloMode configure() @@ -15,13 +15,17 @@ from django.conf import settings from sentry.models.organizationmapping import OrganizationMapping -def exec_run(container, command): - wrapped_command = f'sh -c "{" ".join(command)}"' - exit_code, output = container.exec_run(cmd=wrapped_command, stdout=True, stderr=True) - if exit_code: +def exec_run(container_name: str, command: list[str]) -> bytes: + """Execute a command in a docker container using docker exec.""" + shell_command = " ".join(command) + docker_command = ["docker", "exec", container_name, "sh", "-c", shell_command] + + try: + result = subprocess.run(docker_command, capture_output=True, check=True, shell=False) + return result.stdout + except subprocess.CalledProcessError as e: click.echo("Container operation Failed!") - click.echo(f"Container operation failed with {output}") - return output + click.echo(f"Command failed with {e.stderr}") def split_database(tables: list[str], source: str, destination: str, reset: bool, verbose: bool): @@ -31,71 +35,65 @@ def split_database(tables: list[str], source: str, destination: str, reset: bool command.extend(["-t", table]) command.extend([">", f"/tmp/{destination}-tables.sql"]) - with get_docker_client() as client: - postgres_container = ( - "sentry-postgres-1" - if os.environ.get("USE_OLD_DEVSERVICES") != "1" - else "sentry_postgres" - ) - postgres = client.containers.get(postgres_container) + postgres_container = "sentry-postgres-1" + + if verbose: + click.echo(f">> Running {' '.join(command)}") + exec_run(postgres_container, command) + + if reset: + click.echo(f">> Dropping existing {destination} database") + exec_run(postgres_container, ["dropdb", "-U", "postgres", "--if-exists", destination]) + exec_run(postgres_container, ["createdb", "-U", "postgres", destination]) - if verbose: - click.echo(f">> Running {' '.join(command)}") - exec_run(postgres, command) + citext_command = [ + "psql", + "-U", + "postgres", + destination, + "-c", + "'CREATE EXTENSION IF NOT EXISTS citext'", + ] - if reset: - click.echo(f">> Dropping existing {destination} database") - exec_run(postgres, ["dropdb", "-U", "postgres", "--if-exists", destination]) - exec_run(postgres, ["createdb", "-U", "postgres", destination]) + if verbose: + click.echo(f">> RUNNING: {' '.join(citext_command)}") + exec_run(postgres_container, citext_command) + + # Use the dump file to build control silo tables. + click.echo(f">> Building {destination} database from dump file") + import_command = [ + "psql", + "-U", + "postgres", + destination, + "<", + f"/tmp/{destination}-tables.sql", + ] + if verbose: + click.echo(f">> Running {' '.join(import_command)}") + exec_run(postgres_container, import_command) - citext_command = [ + if destination == "region" and reset: + click.echo(">> Cloning stored procedures") + function_dump = [ "psql", "-U", "postgres", - destination, + source, "-c", - "'CREATE EXTENSION IF NOT EXISTS citext'", + "'\\sf sentry_increment_project_counter'", ] + function_sql = exec_run(postgres_container, function_dump) - if verbose: - click.echo(f">> RUNNING: {' '.join(citext_command)}") - exec_run(postgres, citext_command) - - # Use the dump file to build control silo tables. - click.echo(f">> Building {destination} database from dump file") - import_command = [ + import_function = [ "psql", "-U", "postgres", destination, - "<", - f"/tmp/{destination}-tables.sql", + "-c", + "'" + function_sql.decode("utf8") + "'", ] - if verbose: - click.echo(f">> Running {' '.join(import_command)}") - exec_run(postgres, import_command) - - if destination == "region" and reset: - click.echo(">> Cloning stored procedures") - function_dump = [ - "psql", - "-U", - "postgres", - source, - "-c", - "'\\sf sentry_increment_project_counter'", - ] - function_sql = exec_run(postgres, function_dump) - - import_function = [ - "psql", - "-U", - "postgres", - destination, - "-c", - "'" + function_sql.decode("utf8") + "'", - ] - exec_run(postgres, import_function) + exec_run(postgres_container, import_function) def revise_organization_mappings(legacy_region_name: str): diff --git a/devenv/sync.py b/devenv/sync.py index 8c1631ad353063..57515bfb7c793a 100644 --- a/devenv/sync.py +++ b/devenv/sync.py @@ -109,8 +109,6 @@ def main(context: dict[str, str]) -> int: FRONTEND_ONLY = os.environ.get("SENTRY_DEVENV_FRONTEND_ONLY") is not None SKIP_FRONTEND = os.environ.get("SENTRY_DEVENV_SKIP_FRONTEND") is not None - USE_OLD_DEVSERVICES = os.environ.get("USE_OLD_DEVSERVICES") == "1" - if constants.DARWIN and os.path.exists(f"{constants.root}/bin/colima"): binroot = f"{reporoot}/.devenv/bin" colima.uninstall(binroot) @@ -239,41 +237,20 @@ def main(context: dict[str, str]) -> int: print("Skipping python migrations since SENTRY_DEVENV_FRONTEND_ONLY is set.") return 0 - if USE_OLD_DEVSERVICES: - # Ensure new devservices is not being used, otherwise ports will conflict - proc.run( - (f"{venv_dir}/bin/devservices", "down"), - pathprepend=f"{reporoot}/.devenv/bin", - exit=True, - ) - # TODO: check healthchecks for redis and postgres to short circuit this - proc.run( - ( - f"{venv_dir}/bin/{repo}", - "devservices", - "up", - "redis", - "postgres", - ), - pathprepend=f"{reporoot}/.devenv/bin", - exit=True, - ) - else: - # Ensure old sentry devservices is not being used, otherwise ports will conflict - proc.run( - ( - f"{venv_dir}/bin/{repo}", - "devservices", - "down", - ), - pathprepend=f"{reporoot}/.devenv/bin", - exit=True, - ) - proc.run( - (f"{venv_dir}/bin/devservices", "up", "--mode", "migrations"), - pathprepend=f"{reporoot}/.devenv/bin", - exit=True, - ) + proc.run( + ( + f"{venv_dir}/bin/{repo}", + "devservices", + "down", + ), + pathprepend=f"{reporoot}/.devenv/bin", + exit=True, + ) + proc.run( + (f"{venv_dir}/bin/devservices", "up", "--mode", "migrations"), + pathprepend=f"{reporoot}/.devenv/bin", + exit=True, + ) if not run_procs( repo, @@ -290,9 +267,7 @@ def main(context: dict[str, str]) -> int: ): return 1 - postgres_container = ( - "sentry_postgres" if os.environ.get("USE_OLD_DEVSERVICES") == "1" else "sentry-postgres-1" - ) + postgres_container = "sentry-postgres-1" # faster prerequisite check than starting up sentry and running createuser idempotently stdout = proc.run( diff --git a/scripts/lib.sh b/scripts/lib.sh index 702b5fae34b6af..2f31c0403bcd78 100755 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -8,10 +8,6 @@ # shellcheck disable=SC2001 # https://github.com/koalaman/shellcheck/wiki/SC2001 POSTGRES_CONTAINER="sentry-postgres-1" -USE_OLD_DEVSERVICES=${USE_OLD_DEVSERVICES:-"0"} -if [ "$USE_OLD_DEVSERVICES" == "1" ]; then - POSTGRES_CONTAINER="sentry_postgres" -fi venv_name=".venv" diff --git a/scripts/upgrade-postgres.sh b/scripts/upgrade-postgres.sh index 9f3de7f32a6042..af6695cb2ea9dc 100755 --- a/scripts/upgrade-postgres.sh +++ b/scripts/upgrade-postgres.sh @@ -1,10 +1,6 @@ #!/bin/bash POSTGRES_CONTAINER="sentry-postgres-1" -USE_OLD_DEVSERVICES=${USE_OLD_DEVSERVICES:-"0"} -if [ "$USE_OLD_DEVSERVICES" == "1" ]; then - POSTGRES_CONTAINER="sentry_postgres" -fi OLD_VERSION="9.6" NEW_VERSION="14" diff --git a/src/sentry/runner/commands/devserver.py b/src/sentry/runner/commands/devserver.py index fb2ccf8779fc22..9452dcc2a5dc72 100644 --- a/src/sentry/runner/commands/devserver.py +++ b/src/sentry/runner/commands/devserver.py @@ -377,18 +377,11 @@ def devserver( # Create all topics if the Kafka eventstream is selected if kafka_consumers: - use_old_devservices = os.environ.get("USE_OLD_DEVSERVICES") == "1" - valid_kafka_container_names = ["kafka-kafka-1", "sentry_kafka"] - kafka_container_name = "sentry_kafka" if use_old_devservices else "kafka-kafka-1" - kafka_container_warning_message = ( - f""" + valid_kafka_container_names = ["kafka-kafka-1"] + kafka_container_name = "kafka-kafka-1" + kafka_container_warning_message = f""" Devserver is configured to work with `sentry devservices`. Looks like the `{kafka_container_name}` container is not running. Please run `sentry devservices up kafka` to start it.""" - if use_old_devservices - else f""" - Devserver is configured to work with the revamped devservices. Looks like the `{kafka_container_name}` container is not running. - Please run `devservices up` to start it.""" - ) if not any(name in containers for name in valid_kafka_container_names): raise click.ClickException( f""" diff --git a/src/sentry/runner/commands/devservices.py b/src/sentry/runner/commands/devservices.py deleted file mode 100644 index 76e6024599d39f..00000000000000 --- a/src/sentry/runner/commands/devservices.py +++ /dev/null @@ -1,870 +0,0 @@ -from __future__ import annotations - -import contextlib -import functools -import http -import json # noqa -import os -import shutil -import signal -import subprocess -import sys -import time -import urllib.error -import urllib.request -from collections.abc import Callable, Generator -from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import TYPE_CHECKING, Any, ContextManager, Literal, NamedTuple, overload - -import click - -if TYPE_CHECKING: - import docker - -CI = os.environ.get("CI") is not None -USE_OLD_DEVSERVICES = os.environ.get("USE_OLD_DEVSERVICES") == "1" - -# assigned as a constant so mypy's "unreachable" detection doesn't fail on linux -# https://github.com/python/mypy/issues/12286 -DARWIN = sys.platform == "darwin" - -USE_COLIMA = bool(shutil.which("colima")) and os.environ.get("SENTRY_USE_COLIMA") != "0" -USE_ORBSTACK = ( - os.path.exists("/Applications/OrbStack.app") and os.environ.get("SENTRY_USE_ORBSTACK") != "0" -) - -if USE_ORBSTACK: - USE_COLIMA = False - -if USE_COLIMA: - USE_ORBSTACK = False - -USE_DOCKER_DESKTOP = not USE_COLIMA and not USE_ORBSTACK - -if DARWIN: - if USE_COLIMA: - RAW_SOCKET_PATH = os.path.expanduser("~/.colima/default/docker.sock") - elif USE_ORBSTACK: - RAW_SOCKET_PATH = os.path.expanduser("~/.orbstack/run/docker.sock") - elif USE_DOCKER_DESKTOP: - # /var/run/docker.sock is now gated behind a docker desktop advanced setting - RAW_SOCKET_PATH = os.path.expanduser("~/.docker/run/docker.sock") -else: - RAW_SOCKET_PATH = "/var/run/docker.sock" - - -# Simplified from pre-commit @ fb0ccf3546a9cb34ec3692e403270feb6d6033a2 -@functools.cache -def _gitroot() -> str: - from os.path import abspath - from subprocess import CalledProcessError, run - - try: - proc = run(("git", "rev-parse", "--show-cdup"), check=True, capture_output=True) - root = abspath(proc.stdout.decode().strip()) - except CalledProcessError: - raise SystemExit( - "git failed. Is it installed, and are you in a Git repository directory?", - ) - return root - - -@contextlib.contextmanager -def get_docker_client() -> Generator[docker.DockerClient]: - import docker - - def _client() -> ContextManager[docker.DockerClient]: - return contextlib.closing(docker.DockerClient(base_url=f"unix://{RAW_SOCKET_PATH}")) - - with contextlib.ExitStack() as ctx: - try: - client = ctx.enter_context(_client()) - except docker.errors.DockerException: - if DARWIN: - if USE_COLIMA: - click.echo("Attempting to start colima...") - gitroot = _gitroot() - subprocess.check_call( - ( - # explicitly use repo-local devenv, not the global one - f"{gitroot}/.venv/bin/devenv", - "colima", - "start", - ) - ) - elif USE_DOCKER_DESKTOP: - click.echo("Attempting to start docker...") - subprocess.check_call( - ("open", "-a", "/Applications/Docker.app", "--args", "--unattended") - ) - elif USE_ORBSTACK: - click.echo("Attempting to start orbstack...") - subprocess.check_call( - ("open", "-a", "/Applications/OrbStack.app", "--args", "--unattended") - ) - else: - raise click.ClickException("Make sure docker is running.") - - max_wait = 90 - timeout = time.monotonic() + max_wait - - click.echo(f"Waiting for docker to be ready.... (timeout in {max_wait}s)") - while time.monotonic() < timeout: - time.sleep(1) - try: - client = ctx.enter_context(_client()) - except docker.errors.DockerException: - continue - else: - break - else: - raise click.ClickException("Failed to start docker.") - - yield client - - -@overload -def get_or_create( - client: docker.DockerClient, thing: Literal["network"], name: str -) -> docker.models.networks.Network: ... - - -@overload -def get_or_create( - client: docker.DockerClient, thing: Literal["volume"], name: str -) -> docker.models.volumes.Volume: ... - - -def get_or_create( - client: docker.DockerClient, thing: Literal["network", "volume"], name: str -) -> docker.models.networks.Network | docker.models.volumes.Volume: - from docker.errors import NotFound - - try: - return getattr(client, thing + "s").get(name) - except NotFound: - click.secho(f"> Creating '{name}' {thing}", err=True, fg="yellow") - return getattr(client, thing + "s").create(name) - - -def retryable_pull( - client: docker.DockerClient, image: str, max_attempts: int = 5, platform: str | None = None -) -> None: - from docker.errors import APIError - - current_attempt = 0 - - # `client.images.pull` intermittently fails in CI, and the docker API/docker-py does not give us the relevant error message (i.e. it's not the same error as running `docker pull` from shell) - # As a workaround, let's retry when we hit the ImageNotFound exception. - # - # See https://github.com/docker/docker-py/issues/2101 for more information - while True: - try: - if platform: - client.images.pull(image, platform=platform) - else: - client.images.pull(image) - except APIError: - if current_attempt + 1 >= max_attempts: - raise - current_attempt = current_attempt + 1 - continue - else: - break - - -def ensure_interface(ports: dict[str, int | tuple[str, int]]) -> dict[str, tuple[str, int]]: - # If there is no interface specified, make sure the - # default interface is 127.0.0.1 - rv = {} - for k, v in ports.items(): - if not isinstance(v, tuple): - v = ("127.0.0.1", v) - rv[k] = v - return rv - - -def ensure_docker_cli_context(context: str) -> None: - # this is faster than running docker context use ... - config_file = os.path.expanduser("~/.docker/config.json") - config = {} - - if os.path.exists(config_file): - with open(config_file, "rb") as f: - config = json.loads(f.read()) - - config["currentContext"] = context - - os.makedirs(os.path.dirname(config_file), exist_ok=True) - with open(config_file, "w") as f: - f.write(json.dumps(config)) - - -@click.group() -def devservices() -> None: - """ - Manage dependent development services required for Sentry. - - Do not use in production! - """ - # Disable backend validation so no devservices commands depend on like, - # redis to be already running. - os.environ["SENTRY_SKIP_BACKEND_VALIDATION"] = "1" - - if CI: - click.echo("Assuming docker (CI).") - return - - if not USE_OLD_DEVSERVICES: - return - - if DARWIN: - if USE_DOCKER_DESKTOP: - click.echo("Using docker desktop.") - ensure_docker_cli_context("desktop-linux") - if USE_COLIMA: - click.echo("Using colima.") - ensure_docker_cli_context("colima") - if USE_ORBSTACK: - click.echo("Using orbstack.") - ensure_docker_cli_context("orbstack") - - -@devservices.command() -@click.option("--project", default="sentry") -@click.argument("service", nargs=1) -def attach(project: str, service: str) -> None: - """ - Run a single devservice in the foreground. - - Accepts a single argument, the name of the service to spawn. The service - will run with output printed to your terminal, and the ability to kill it - with ^C. This is used in devserver. - - Note: This does not update images, you will have to use `devservices up` - for that. - """ - from sentry.runner import configure - - configure() - - containers = _prepare_containers(project, silent=True) - if service not in containers: - raise click.ClickException(f"Service `{service}` is not known or not enabled.") - - with get_docker_client() as docker_client: - container = _start_service( - docker_client, - service, - containers, - project, - always_start=True, - ) - - if container is None: - raise click.ClickException(f"No containers found for service `{service}`.") - - def exit_handler(*_: Any) -> None: - try: - click.echo(f"Stopping {service}") - container.stop() - click.echo(f"Removing {service}") - container.remove() - except KeyboardInterrupt: - pass - - signal.signal(signal.SIGINT, exit_handler) - signal.signal(signal.SIGTERM, exit_handler) - - for line in container.logs(stream=True, since=int(time.time() - 20)): - click.echo(line, nl=False) - - -@devservices.command() -@click.argument("services", nargs=-1) -@click.option("--project", default="sentry") -@click.option("--exclude", multiple=True, help="Service to ignore and not run. Repeatable option.") -@click.option( - "--skip-only-if", is_flag=True, default=False, help="Skip 'only_if' checks for services" -) -@click.option( - "--recreate", is_flag=True, default=False, help="Recreate containers that are already running." -) -def up( - services: list[str], - project: str, - exclude: list[str], - skip_only_if: bool, - recreate: bool, -) -> None: - """ - Run/update all devservices in the background. - - The default is everything, however you may pass positional arguments to specify - an explicit list of services to bring up. - - You may also exclude services, for example: --exclude redis --exclude postgres. - """ - from sentry.runner import configure - - if not USE_OLD_DEVSERVICES: - click.secho( - "WARNING: `sentry devservices up` is deprecated. Please use `devservices up` instead. For more info about the revamped devservices, see https://github.com/getsentry/devservices.", - fg="yellow", - ) - return - - configure() - - containers = _prepare_containers( - project, skip_only_if=(skip_only_if or len(services) > 0), silent=True - ) - selected_services = set() - - if services: - for service in services: - if service not in containers: - click.secho( - f"Service `{service}` is not known or not enabled.\n", - err=True, - fg="red", - ) - click.secho( - "Services that are available:\n" + "\n".join(containers.keys()) + "\n", err=True - ) - raise click.Abort() - selected_services.add(service) - else: - selected_services = set(containers.keys()) - - for service in exclude: - if service not in containers: - click.secho(f"Service `{service}` is not known or not enabled.\n", err=True, fg="red") - click.secho( - "Services that are available:\n" + "\n".join(containers.keys()) + "\n", err=True - ) - raise click.Abort() - selected_services.remove(service) - - with get_docker_client() as docker_client: - get_or_create(docker_client, "network", project) - - with ThreadPoolExecutor(max_workers=len(selected_services)) as executor: - futures = [] - for name in selected_services: - futures.append( - executor.submit( - _start_service, - docker_client, - name, - containers, - project, - False, - recreate, - ) - ) - for future in as_completed(futures): - try: - future.result() - except Exception as e: - click.secho(f"> Failed to start service: {e}", err=True, fg="red") - raise - - # Check health of services. Seperate from _start_services - # in case there are dependencies needed for the health - # check (for example: kafka's healthcheck requires zookeeper) - with ThreadPoolExecutor(max_workers=len(selected_services)) as executor: - futures = [] - for name in selected_services: - futures.append( - executor.submit( - check_health, - name, - containers[name], - ) - ) - for future in as_completed(futures): - try: - future.result() - except Exception as e: - click.secho(f"> Failed to check health: {e}", err=True, fg="red") - raise - - -def _prepare_containers( - project: str, skip_only_if: bool = False, silent: bool = False -) -> dict[str, Any]: - from django.conf import settings - - from sentry import options as sentry_options - - containers = {} - - for name, option_builder in settings.SENTRY_DEVSERVICES.items(): - options = option_builder(settings, sentry_options) - only_if = options.pop("only_if", True) - - if not skip_only_if and not only_if: - if not silent: - click.secho(f"! Skipping {name} due to only_if condition", err=True, fg="cyan") - continue - - options["network"] = project - options["detach"] = True - options["name"] = project + "_" + name - options.setdefault("ports", {}) - options.setdefault("environment", {}) - # set policy to unless-stopped to avoid automatically restarting containers on boot - # this is important given you can start multiple sets of containers that can conflict - # with each other - options.setdefault("restart_policy", {"Name": "unless-stopped"}) - options["ports"] = ensure_interface(options["ports"]) - options["extra_hosts"] = {"host.docker.internal": "host-gateway"} - containers[name] = options - - # keys are service names - # a service has 1 container exactly, the container name being value["name"] - return containers - - -@overload -def _start_service( - client: docker.DockerClient, - name: str, - containers: dict[str, Any], - project: str, - always_start: Literal[False] = ..., - recreate: bool = False, -) -> docker.models.containers.Container: ... - - -@overload -def _start_service( - client: docker.DockerClient, - name: str, - containers: dict[str, Any], - project: str, - always_start: bool = False, - recreate: bool = False, -) -> docker.models.containers.Container | None: ... - - -def _start_service( - client: docker.DockerClient, - name: str, - containers: dict[str, Any], - project: str, - always_start: bool = False, - recreate: bool = False, -) -> docker.models.containers.Container | None: - from docker.errors import NotFound - - options = containers[name] - - # If a service is associated with the devserver, then do not run the created container. - # This was mainly added since it was not desirable for nginx to occupy port 8000 on the - # first "devservices up". - # Nowadays that nginx is gone again, it's still nice to be able to shut - # down services within devserver. - # See https://github.com/getsentry/sentry/pull/18362#issuecomment-616785458 - with_devserver = options.pop("with_devserver", False) - - # Two things call _start_service. - # devservices up, and devservices attach. - # Containers that should be started on-demand with devserver - # should ONLY be started via the latter, which sets `always_start`. - if with_devserver and not always_start: - click.secho( - f"> Not starting container '{options['name']}' because it should be started on-demand with devserver.", - fg="yellow", - ) - # XXX: if always_start=False, do not expect to have a container returned 100% of the time. - return None - - container = None - try: - container = client.containers.get(options["name"]) - except NotFound: - pass - - if container is not None: - if not recreate and container.status == "running": - click.secho(f"> Container '{options['name']}' is already running", fg="yellow") - return container - - click.secho(f"> Stopping container '{container.name}'", fg="yellow") - container.stop() - click.secho(f"> Removing container '{container.name}'", fg="yellow") - container.remove() - - for key, value in list(options["environment"].items()): - options["environment"][key] = value.format(containers=containers) - - click.secho(f"> Pulling image '{options['image']}'", fg="green") - retryable_pull(client, options["image"], platform=options.get("platform")) - - for mount in list(options.get("volumes", {}).keys()): - if "/" not in mount: - get_or_create(client, "volume", project + "_" + mount) - options["volumes"][project + "_" + mount] = options["volumes"].pop(mount) - - listening = "" - if options["ports"]: - listening = "(listening: %s)" % ", ".join(map(str, options["ports"].values())) - - click.secho(f"> Creating container '{options['name']}'", fg="yellow") - container = client.containers.create(**options) - click.secho(f"> Starting container '{container.name}' {listening}", fg="yellow") - container.start() - return container - - -@devservices.command() -@click.option("--project", default="sentry") -@click.argument("service", nargs=-1) -def down(project: str, service: list[str]) -> None: - """ - Shut down services without deleting their underlying data. - Useful if you want to temporarily relieve resources on your computer. - - The default is everything, however you may pass positional arguments to specify - an explicit list of services to bring down. - """ - - def _down(container: docker.models.containers.Container) -> None: - click.secho(f"> Stopping '{container.name}' container", fg="red") - container.stop() - click.secho(f"> Removing '{container.name}' container", fg="red") - container.remove() - - containers = [] - prefix = f"{project}_" - - with get_docker_client() as docker_client: - for container in docker_client.containers.list(all=True): - if not container.name.startswith(prefix): - continue - if service and not container.name[len(prefix) :] in service: - continue - containers.append(container) - - with ThreadPoolExecutor(max_workers=len(containers) or 1) as executor: - futures = [] - for container in containers: - futures.append(executor.submit(_down, container)) - for future in as_completed(futures): - try: - future.result() - except Exception as e: - click.secho(f"> Failed to stop service: {e}", err=True, fg="red") - raise - - -@devservices.command() -@click.option("--project", default="sentry") -@click.argument("services", nargs=-1) -def rm(project: str, services: list[str]) -> None: - """ - Shut down and delete all services and associated data. - Useful if you'd like to start with a fresh slate. - - The default is everything, however you may pass positional arguments to specify - an explicit list of services to remove. - """ - from docker.errors import NotFound - - from sentry.runner import configure - - configure() - - containers = _prepare_containers(project, skip_only_if=len(services) > 0, silent=True) - - if services: - selected_containers = {} - for service in services: - # XXX: This code is also fairly duplicated in here at this point, so dedupe in the future. - if service not in containers: - click.secho( - f"Service `{service}` is not known or not enabled.\n", - err=True, - fg="red", - ) - click.secho( - "Services that are available:\n" + "\n".join(containers.keys()) + "\n", err=True - ) - raise click.Abort() - selected_containers[service] = containers[service] - containers = selected_containers - - click.confirm( - """ -This will delete these services and all of their data: - -%s - -Are you sure you want to continue?""" - % "\n".join(containers.keys()), - abort=True, - ) - - with get_docker_client() as docker_client: - volume_to_service = {} - for service_name, container_options in containers.items(): - try: - container = docker_client.containers.get(container_options["name"]) - except NotFound: - click.secho( - "> WARNING: non-existent container '%s'" % container_options["name"], - err=True, - fg="yellow", - ) - continue - - click.secho("> Stopping '%s' container" % container_options["name"], err=True, fg="red") - container.stop() - click.secho("> Removing '%s' container" % container_options["name"], err=True, fg="red") - container.remove() - for volume in container_options.get("volumes") or (): - volume_to_service[volume] = service_name - - prefix = project + "_" - - for volume in docker_client.volumes.list(): - if volume.name.startswith(prefix): - local_name = volume.name[len(prefix) :] - if not services or volume_to_service.get(local_name) in services: - click.secho("> Removing '%s' volume" % volume.name, err=True, fg="red") - volume.remove() - - if not services: - try: - network = docker_client.networks.get(project) - except NotFound: - pass - else: - click.secho("> Removing '%s' network" % network.name, err=True, fg="red") - network.remove() - - -def check_health(service_name: str, options: dict[str, Any]) -> None: - healthcheck = service_healthchecks.get(service_name, None) - if healthcheck is None: - return - - click.secho(f"> Checking container health '{service_name}'", fg="yellow") - - def hc() -> None: - healthcheck.check(options) - - try: - run_with_retries( - hc, - healthcheck.retries, - healthcheck.timeout, - f"Health check for '{service_name}' failed", - ) - click.secho(f" > '{service_name}' is healthy", fg="green") - except subprocess.CalledProcessError: - click.secho(f" > '{service_name}' is not healthy", fg="red") - raise - - -def run_with_retries( - cmd: Callable[[], object], retries: int = 3, timeout: int = 5, message: str = "Command failed" -) -> None: - for retry in range(1, retries + 1): - try: - cmd() - except ( - subprocess.CalledProcessError, - urllib.error.HTTPError, - http.client.RemoteDisconnected, - ): - if retry == retries: - raise - else: - click.secho( - f" > {message}, retrying in {timeout}s (attempt {retry+1} of {retries})...", - fg="yellow", - ) - time.sleep(timeout) - else: - return - - -def check_postgres(options: dict[str, Any]) -> None: - subprocess.run( - ( - "docker", - "exec", - options["name"], - "pg_isready", - "-U", - "postgres", - ), - check=True, - capture_output=True, - text=True, - ) - - -def check_rabbitmq(options: dict[str, Any]) -> None: - subprocess.run( - ( - "docker", - "exec", - options["name"], - "rabbitmq-diagnostics", - "-q", - "ping", - ), - check=True, - capture_output=True, - text=True, - ) - - -def check_redis(options: dict[str, Any]) -> None: - subprocess.run( - ( - "docker", - "exec", - options["name"], - "redis-cli", - "ping", - ), - check=True, - capture_output=True, - text=True, - ) - - -def check_vroom(options: dict[str, Any]) -> None: - (port,) = options["ports"].values() - - # Vroom is a slim debian based image and does not have curl, wget or - # python3. Check health with a simple request on the host machine. - urllib.request.urlopen(f"http://{port[0]}:{port[1]}/health", timeout=1) - - -def check_clickhouse(options: dict[str, Any]) -> None: - port = options["ports"]["8123/tcp"] - subprocess.run( - ( - "docker", - "exec", - options["name"], - # Using wget instead of curl as that is what is available - # in the clickhouse image - "wget", - f"http://{port[0]}:{port[1]}/ping", - ), - check=True, - capture_output=True, - text=True, - ) - - -def check_kafka(options: dict[str, Any]) -> None: - (port,) = options["ports"].values() - subprocess.run( - ( - "docker", - "exec", - options["name"], - "kafka-topics", - "--bootstrap-server", - # Port is a tuple of (127.0.0.1, ) - f"{port[0]}:{port[1]}", - "--list", - ), - check=True, - capture_output=True, - text=True, - ) - - -def check_symbolicator(options: dict[str, Any]) -> None: - (port,) = options["ports"].values() - subprocess.run( - ( - "docker", - "exec", - options["name"], - "curl", - f"http://{port[0]}:{port[1]}/healthcheck", - ), - check=True, - capture_output=True, - text=True, - ) - - -def python_call_url_prog(url: str) -> str: - return f""" -import urllib.request -try: - req = urllib.request.urlopen({url!r}, timeout=1) -except Exception as e: - raise SystemExit(f'service is not ready: {{e}}') -else: - print('service is ready!') -""" - - -def check_chartcuterie(options: dict[str, Any]) -> None: - # Chartcuterie binds the internal port to a different port - internal_port = 9090 - port = options["ports"][f"{internal_port}/tcp"] - url = f"http://{port[0]}:{internal_port}/api/chartcuterie/healthcheck/live" - subprocess.run( - ( - "docker", - "exec", - options["name"], - "python3", - "-uc", - python_call_url_prog(url), - ), - check=True, - capture_output=True, - text=True, - ) - - -def check_snuba(options: dict[str, Any]) -> None: - from django.conf import settings - - url = f"{settings.SENTRY_SNUBA}/health_envoy" - subprocess.run( - ( - "docker", - "exec", - options["name"], - "python3", - "-uc", - python_call_url_prog(url), - ), - check=True, - capture_output=True, - text=True, - ) - - -class ServiceHealthcheck(NamedTuple): - check: Callable[[dict[str, Any]], None] - retries: int = 3 - timeout: int = 5 - - -service_healthchecks: dict[str, ServiceHealthcheck] = { - "postgres": ServiceHealthcheck(check=check_postgres), - "rabbitmq": ServiceHealthcheck(check=check_rabbitmq), - "redis": ServiceHealthcheck(check=check_redis), - "clickhouse": ServiceHealthcheck(check=check_clickhouse), - "kafka": ServiceHealthcheck(check=check_kafka), - "vroom": ServiceHealthcheck(check=check_vroom), - "symbolicator": ServiceHealthcheck(check=check_symbolicator), - "chartcuterie": ServiceHealthcheck(check=check_chartcuterie), - "snuba": ServiceHealthcheck(check=check_snuba, retries=12, timeout=10), -} diff --git a/src/sentry/testutils/pytest/relay.py b/src/sentry/testutils/pytest/relay.py index 4d9b6c1b12637e..ca13dbcf88f049 100644 --- a/src/sentry/testutils/pytest/relay.py +++ b/src/sentry/testutils/pytest/relay.py @@ -69,7 +69,6 @@ def relay_server_setup(live_server, tmpdir_factory): relay_port = 33331 redis_db = TEST_REDIS_DB - use_old_devservices = environ.get("USE_OLD_DEVSERVICES", "0") == "1" from sentry.relay import projectconfig_cache from sentry.relay.projectconfig_cache.redis import RedisProjectConfigCache @@ -81,8 +80,8 @@ def relay_server_setup(live_server, tmpdir_factory): template_vars = { "SENTRY_HOST": f"http://host.docker.internal:{port}/", "RELAY_PORT": relay_port, - "KAFKA_HOST": "sentry_kafka" if use_old_devservices else "kafka", - "REDIS_HOST": "sentry_redis" if use_old_devservices else "redis", + "KAFKA_HOST": "kafka", + "REDIS_HOST": "redis", "REDIS_DB": redis_db, } @@ -107,7 +106,7 @@ def relay_server_setup(live_server, tmpdir_factory): options = { "image": RELAY_TEST_IMAGE, "ports": {"%s/tcp" % relay_port: relay_port}, - "network": "sentry" if use_old_devservices else "devservices", + "network": "devservices", "detach": True, "name": container_name, "volumes": {config_path: {"bind": "/etc/relay"}},