diff --git a/.envrc b/.envrc index 9a12b7dfa033e9..74a14fb3a3f6ba 100644 --- a/.envrc +++ b/.envrc @@ -122,7 +122,6 @@ else exec > >(tee "$_SENTRY_LOG_FILE") exec 2>&1 debug "Development errors will be reported to Sentry.io. If you wish to opt-out, set SENTRY_DEVENV_NO_REPORT as an env variable." - # This will allow `sentry devservices` errors to be reported export SENTRY_DEVSERVICES_DSN=https://23670f54c6254bfd9b7de106637808e9@o1.ingest.sentry.io/1492057 fi diff --git a/Makefile b/Makefile index 1f9bc9a9902445..a30f06ef07207d 100644 --- a/Makefile +++ b/Makefile @@ -173,7 +173,7 @@ test-tools: @echo "" # JavaScript relay tests are meant to be run within Symbolicator test suite, as they are parametrized to verify both processing pipelines during migration process. -# Running Locally: Run `sentry devservices up kafka` before starting these tests +# Running Locally: Run `devservices up` before starting these tests test-symbolicator: @echo "--> Running symbolicator tests" python3 -b -m pytest tests/symbolicator -vv --cov . --cov-report="xml:.artifacts/symbolicator.coverage.xml" --junit-xml=.artifacts/symbolicator.junit.xml -o junit_suite_name=symbolicator diff --git a/bin/split-silo-database b/bin/split-silo-database index 5273084ad34299..49dc924c917aa0 100755 --- a/bin/split-silo-database +++ b/bin/split-silo-database @@ -1,5 +1,4 @@ #!/usr/bin/env python -import os import click from django.apps import apps @@ -32,12 +31,7 @@ def split_database(tables: list[str], source: str, destination: str, reset: bool 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 = client.containers.get("sentry-postgres-1") if verbose: click.echo(f">> Running {' '.join(command)}") diff --git a/config/chartcuterie/.gitkeep b/config/chartcuterie/.gitkeep deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/config/clickhouse/dist_config.xml b/config/clickhouse/dist_config.xml deleted file mode 100644 index a2f21d89412a7f..00000000000000 --- a/config/clickhouse/dist_config.xml +++ /dev/null @@ -1,20 +0,0 @@ - - 0.3 - - - - - - localhost - 9000 - - - - - - - 1 - 1 - - - diff --git a/config/clickhouse/loc_config.xml b/config/clickhouse/loc_config.xml deleted file mode 100644 index 327d60661b29da..00000000000000 --- a/config/clickhouse/loc_config.xml +++ /dev/null @@ -1,6 +0,0 @@ - - 0.3 - - 1 - - diff --git a/config/relay/config.yml b/config/relay/config.yml deleted file mode 100644 index 6674d442063cf9..00000000000000 --- a/config/relay/config.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -relay: - upstream: 'http://host.docker.internal:8001/' - host: 0.0.0.0 - port: 7899 -logging: - level: INFO - enable_backtraces: false -limits: - shutdown_timeout: 0 -processing: - enabled: true - kafka_config: - - {name: 'bootstrap.servers', value: 'sentry_kafka:9093'} - # The maximum attachment chunk size is 1MB. Together with some meta data, - # messages will never get larger than 2MB in total. - - {name: 'message.max.bytes', value: 2097176} - topics: - metrics_transactions: ingest-performance-metrics - metrics_sessions: ingest-metrics - spans: ingest-spans - redis: redis://sentry_redis:6379 diff --git a/config/relay/credentials.json b/config/relay/credentials.json deleted file mode 100644 index 1d37e82fd62789..00000000000000 --- a/config/relay/credentials.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "secret_key": "OxE6Du8quMxWj19f7YDCpIxm6XyU9nWGQJkMWFlkchA", - "public_key": "SMSesqan65THCV6M4qs4kBzPai60LzuDn-xNsvYpuP8", - "id": "88888888-4444-4444-8444-cccccccccccc" -} diff --git a/config/symbolicator/config.yml b/config/symbolicator/config.yml deleted file mode 100644 index 290d752a6dd04c..00000000000000 --- a/config/symbolicator/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -bind: '0.0.0.0:3021' -logging: - level: 'debug' - format: 'pretty' - enable_backtraces: true - -# explicitly disable caches as it's not something we want in tests. in -# development it may be less ideal. perhaps we should do the same thing as we -# do with relay one day (one container per test/session), although that will be -# slow -cache_dir: null diff --git a/devenv/sync.py b/devenv/sync.py index f6e88f9a30eb41..806dfd4cfeb87c 100644 --- a/devenv/sync.py +++ b/devenv/sync.py @@ -143,8 +143,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) @@ -264,41 +262,11 @@ 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/devservices", "up", "--mode", "migrations"), + pathprepend=f"{reporoot}/.devenv/bin", + exit=True, + ) if not run_procs( repo, @@ -315,16 +283,12 @@ 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" - ) - # faster prerequisite check than starting up sentry and running createuser idempotently stdout = proc.run( ( "docker", "exec", - postgres_container, + "sentry-postgres-1", "psql", "sentry", "postgres", diff --git a/scripts/lib.sh b/scripts/lib.sh index 702b5fae34b6af..cbdbf39844938c 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" @@ -44,7 +40,7 @@ init-config() { } run-dependent-services() { - sentry devservices up + devservices up } create-db() { 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/conf/server.py b/src/sentry/conf/server.py index 4729196954a8c9..01f89e249dc11d 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -227,20 +227,8 @@ def env( NODE_MODULES_ROOT = os.path.normpath(NODE_MODULES_ROOT) -DEVSERVICES_CONFIG_DIR = os.path.normpath( - os.path.join(PROJECT_ROOT, os.pardir, os.pardir, "config") -) - SENTRY_DISTRIBUTED_CLICKHOUSE_TABLES = False -RELAY_CONFIG_DIR = os.path.join(DEVSERVICES_CONFIG_DIR, "relay") - -SYMBOLICATOR_CONFIG_DIR = os.path.join(DEVSERVICES_CONFIG_DIR, "symbolicator") - -# XXX(epurkhiser): The generated chartucterie config.js file will be stored -# here. This directory may not exist until that file is generated. -CHARTCUTERIE_CONFIG_DIR = os.path.join(DEVSERVICES_CONFIG_DIR, "chartcuterie") - sys.path.insert(0, os.path.normpath(os.path.join(PROJECT_ROOT, os.pardir))) DATABASES = { @@ -2092,271 +2080,6 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: # This flag activates the objectstore in devservices SENTRY_USE_OBJECTSTORE = False -# SENTRY_DEVSERVICES = { -# "service-name": lambda settings, options: ( -# { -# "image": "image-name:version", -# # optional ports to expose -# "ports": {"internal-port/tcp": external-port}, -# # optional command -# "command": ["exit 1"], -# optional mapping of volumes -# "volumes": {"volume-name": {"bind": "/path/in/container"}}, -# # optional statement to test if service should run -# "only_if": lambda settings, options: True, -# # optional environment variables -# "environment": { -# "ENV_VAR": "1", -# } -# } -# ) -# } - - -SENTRY_DEVSERVICES: dict[str, Callable[[Any, Any], dict[str, Any]]] = { - "redis": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine", - "ports": {"6379/tcp": 6379}, - "command": [ - "redis-server", - "--appendonly", - "yes", - "--save", - "60", - "20", - "--auto-aof-rewrite-percentage", - "100", - "--auto-aof-rewrite-min-size", - "64mb", - ], - "volumes": {"redis": {"bind": "/data"}}, - } - ), - "redis-cluster": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/docker-redis-cluster:7.0.10", - "ports": {f"700{idx}/tcp": f"700{idx}" for idx in range(6)}, - "volumes": {"redis-cluster": {"bind": "/redis-data"}}, - "environment": {"IP": "0.0.0.0"}, - "only_if": settings.SENTRY_DEV_USE_REDIS_CLUSTER, - } - ), - "rabbitmq": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/image-mirror-library-rabbitmq:3-management", - "ports": {"5672/tcp": 5672, "15672/tcp": 15672}, - "environment": {"IP": "0.0.0.0"}, - "only_if": settings.SENTRY_DEV_USE_RABBITMQ, - } - ), - "postgres": lambda settings, options: ( - { - "image": f"ghcr.io/getsentry/image-mirror-library-postgres:{PG_VERSION}-alpine", - "ports": {"5432/tcp": 5432}, - "environment": { - "POSTGRES_DB": "sentry", - "POSTGRES_HOST_AUTH_METHOD": "trust", - }, - "volumes": { - "postgres": {"bind": "/var/lib/postgresql/data"}, - "wal2json": {"bind": "/wal2json"}, - }, - "command": [ - "postgres", - "-c", - "wal_level=logical", - "-c", - "max_replication_slots=1", - "-c", - "max_wal_senders=1", - ], - } - ), - "kafka": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/image-mirror-confluentinc-cp-kafka:7.5.0", - "ports": {"9092/tcp": 9092}, - # https://docs.confluent.io/platform/current/installation/docker/config-reference.html#cp-kakfa-example - "environment": { - "KAFKA_PROCESS_ROLES": "broker,controller", - "KAFKA_CONTROLLER_QUORUM_VOTERS": "1@127.0.0.1:29093", - "KAFKA_CONTROLLER_LISTENER_NAMES": "CONTROLLER", - "KAFKA_NODE_ID": "1", - "CLUSTER_ID": "MkU3OEVBNTcwNTJENDM2Qk", - "KAFKA_LISTENERS": "PLAINTEXT://0.0.0.0:29092,INTERNAL://0.0.0.0:9093,EXTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:29093", - "KAFKA_ADVERTISED_LISTENERS": "PLAINTEXT://127.0.0.1:29092,INTERNAL://sentry_kafka:9093,EXTERNAL://127.0.0.1:9092", - "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP": "PLAINTEXT:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT", - "KAFKA_INTER_BROKER_LISTENER_NAME": "PLAINTEXT", - "KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR": "1", - "KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS": "1", - "KAFKA_LOG_RETENTION_HOURS": "24", - "KAFKA_MESSAGE_MAX_BYTES": "50000000", - "KAFKA_MAX_REQUEST_SIZE": "50000000", - }, - "volumes": {"kafka": {"bind": "/var/lib/kafka/data"}}, - "only_if": "kafka" in settings.SENTRY_EVENTSTREAM - or settings.SENTRY_USE_RELAY - or settings.SENTRY_DEV_PROCESS_SUBSCRIPTIONS - or settings.SENTRY_USE_PROFILING, - } - ), - "clickhouse": lambda settings, options: ( - { - "image": ( - "ghcr.io/getsentry/image-mirror-altinity-clickhouse-server:23.8.11.29.altinitystable" - ), - "ports": {"9000/tcp": 9000, "9009/tcp": 9009, "8123/tcp": 8123}, - "ulimits": [{"name": "nofile", "soft": 262144, "hard": 262144}], - # The arm image does not properly load the MAX_MEMORY_USAGE_RATIO - # from the environment in loc_config.xml, thus, hard-coding it there - "volumes": { - ( - "clickhouse_dist" - if settings.SENTRY_DISTRIBUTED_CLICKHOUSE_TABLES - else "clickhouse" - ): {"bind": "/var/lib/clickhouse"}, - os.path.join( - settings.DEVSERVICES_CONFIG_DIR, - "clickhouse", - ( - "dist_config.xml" - if settings.SENTRY_DISTRIBUTED_CLICKHOUSE_TABLES - else "loc_config.xml" - ), - ): {"bind": "/etc/clickhouse-server/config.d/sentry.xml"}, - }, - } - ), - "snuba": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/snuba:latest", - "ports": {"1218/tcp": 1218, "1219/tcp": 1219}, - "command": ["devserver"] - + (["--no-workers"] if "snuba" in settings.SENTRY_EVENTSTREAM else []), - "environment": { - "PYTHONUNBUFFERED": "1", - "SNUBA_SETTINGS": "docker", - "DEBUG": "1", - "CLICKHOUSE_HOST": "{containers[clickhouse][name]}", - "CLICKHOUSE_PORT": "9000", - "CLICKHOUSE_HTTP_PORT": "8123", - "DEFAULT_BROKERS": ( - "" - if "snuba" in settings.SENTRY_EVENTSTREAM - else "{containers[kafka][name]}:9093" - ), - "REDIS_HOST": "{containers[redis][name]}", - "REDIS_PORT": "6379", - "REDIS_DB": "1", - "ENABLE_SENTRY_METRICS_DEV": "1" if settings.SENTRY_USE_METRICS_DEV else "", - "ENABLE_PROFILES_CONSUMER": "1" if settings.SENTRY_USE_PROFILING else "", - "ENABLE_SPANS_CONSUMER": "1" if settings.SENTRY_USE_SPANS else "", - "ENABLE_ISSUE_OCCURRENCE_CONSUMER": ( - "1" if settings.SENTRY_USE_ISSUE_OCCURRENCE else "" - ), - "ENABLE_AUTORUN_MIGRATION_SEARCH_ISSUES": "1", - # TODO: remove setting - "ENABLE_GROUP_ATTRIBUTES_CONSUMER": ( - "1" if settings.SENTRY_USE_GROUP_ATTRIBUTES else "" - ), - }, - "only_if": "snuba" in settings.SENTRY_EVENTSTREAM - or "kafka" in settings.SENTRY_EVENTSTREAM, - # we don't build linux/arm64 snuba images anymore - # apple silicon users should have working emulation under colima 0.6.2 - # or docker desktop - "platform": "linux/amd64", - } - ), - "taskbroker": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/taskbroker:latest", - "ports": {"50051/tcp": 50051}, - "environment": { - "TASKBROKER_KAFKA_CLUSTER": ( - "sentry_kafka" - if os.environ.get("USE_OLD_DEVSERVICES") == "1" - else "kafka-kafka-1" - ), - }, - "only_if": settings.SENTRY_USE_TASKBROKER, - "platform": "linux/amd64", - } - ), - "bigtable": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/cbtemulator:d28ad6b63e461e8c05084b8c83f1c06627068c04", - "ports": {"8086/tcp": 8086}, - # NEED_BIGTABLE is set by CI so we don't have to pass - # --skip-only-if when compiling which services to run. - "only_if": os.environ.get("NEED_BIGTABLE", False) - or "bigtable" in settings.SENTRY_NODESTORE, - } - ), - "memcached": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/image-mirror-library-memcached:1.5-alpine", - "ports": {"11211/tcp": 11211}, - "only_if": "memcached" in settings.CACHES.get("default", {}).get("BACKEND"), - } - ), - "symbolicator": lambda settings, options: ( - { - "image": "us-central1-docker.pkg.dev/sentryio/symbolicator/image:nightly", - "ports": {"3021/tcp": 3021}, - "volumes": {settings.SYMBOLICATOR_CONFIG_DIR: {"bind": "/etc/symbolicator"}}, - "command": ["run", "--config", "/etc/symbolicator/config.yml"], - "only_if": options.get("symbolicator.enabled"), - } - ), - "relay": lambda settings, options: ( - { - "image": "us-central1-docker.pkg.dev/sentryio/relay/relay:nightly", - "ports": {"7899/tcp": settings.SENTRY_RELAY_PORT}, - "volumes": {settings.RELAY_CONFIG_DIR: {"bind": "/etc/relay"}}, - "command": ["run", "--config", "/etc/relay"], - "only_if": bool(os.environ.get("SENTRY_USE_RELAY", settings.SENTRY_USE_RELAY)), - "with_devserver": True, - } - ), - "chartcuterie": lambda settings, options: ( - { - "image": "us-central1-docker.pkg.dev/sentryio/chartcuterie/image:latest", - "volumes": {settings.CHARTCUTERIE_CONFIG_DIR: {"bind": "/etc/chartcuterie"}}, - "environment": { - "CHARTCUTERIE_CONFIG": "/etc/chartcuterie/config.js", - "CHARTCUTERIE_CONFIG_POLLING": "true", - }, - "ports": {"9090/tcp": 7901}, - # NEED_CHARTCUTERIE is set by CI so we don't have to pass --skip-only-if when compiling which services to run. - "only_if": os.environ.get("NEED_CHARTCUTERIE", False) - or options.get("chart-rendering.enabled"), - } - ), - "vroom": lambda settings, options: ( - { - "image": "us-central1-docker.pkg.dev/sentryio/vroom/vroom:latest", - "volumes": {"profiles": {"bind": "/var/lib/sentry-profiles"}}, - "environment": { - "SENTRY_KAFKA_BROKERS_PROFILING": "{containers[kafka][name]}:9093", - "SENTRY_KAFKA_BROKERS_OCCURRENCES": "{containers[kafka][name]}:9093", - "SENTRY_SNUBA_HOST": "http://{containers[snuba][name]}:1218", - }, - "ports": {"8085/tcp": 8085}, - "only_if": settings.SENTRY_USE_PROFILING, - } - ), - "objectstore": lambda settings, options: ( - { - "image": "ghcr.io/getsentry/objectstore:latest", - "ports": {"8888/tcp": 8888}, - "environment": {}, - "only_if": settings.SENTRY_USE_OBJECTSTORE, - } - ), -} - # Max file size for serialized file uploads in API SENTRY_MAX_SERIALIZED_FILE_SIZE = 5000000 diff --git a/src/sentry/runner/commands/devserver.py b/src/sentry/runner/commands/devserver.py index 338bee6ffee0bd..180ee9f7e320a6 100644 --- a/src/sentry/runner/commands/devserver.py +++ b/src/sentry/runner/commands/devserver.py @@ -349,10 +349,8 @@ def devserver( kafka_consumers.add("ingest-occurrences") # Check if Kafka is available and create all topics if it is running - 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_is_running = any(name in containers for name in valid_kafka_container_names) + kafka_container_name = "kafka-kafka-1" + kafka_is_running = kafka_container_name in containers if kafka_is_running: from sentry_kafka_schemas import list_topics @@ -366,15 +364,9 @@ def devserver( # Set up Kafka consumers if they are configured if kafka_consumers: - 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. + kafka_container_warning_message = f""" + Devserver is configured to work with devservices. Looks like the `{kafka_container_name}` container is not running. Please run `devservices up` to start it.""" - ) if not kafka_is_running: raise click.ClickException( f""" diff --git a/src/sentry/runner/commands/devservices.py b/src/sentry/runner/commands/devservices.py index 92aa9ed4b09597..58b21d638556fe 100644 --- a/src/sentry/runner/commands/devservices.py +++ b/src/sentry/runner/commands/devservices.py @@ -1,20 +1,11 @@ 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 +from collections.abc import Generator +from typing import TYPE_CHECKING, ContextManager import click @@ -22,53 +13,19 @@ 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") + RAW_SOCKET_PATH = os.path.expanduser("~/.colima/default/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 - - +# NOTE: we can delete the docker python client dependency if we port all usage of this +# to docker cli calls @contextlib.contextmanager def get_docker_client() -> Generator[docker.DockerClient]: import docker @@ -81,790 +38,17 @@ def _client() -> ContextManager[docker.DockerClient]: 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") - ) + raise click.ClickException( + "Make sure colima is running. Run `devenv colima start`." + ) 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: - health_futures = [] - for name in selected_services: - health_futures.append( - executor.submit( - check_health, - name, - containers[name], - ) - ) - for health_future in as_completed(health_futures): - try: - health_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), -} +# compatibility stub +@click.command() +@click.argument("args", nargs=-1) +def devservices(args: tuple[str, ...]) -> None: + return diff --git a/src/sentry/testutils/pytest/relay.py b/src/sentry/testutils/pytest/relay.py index de488d5009e15c..f1551226ca6024 100644 --- a/src/sentry/testutils/pytest/relay.py +++ b/src/sentry/testutils/pytest/relay.py @@ -69,7 +69,7 @@ def relay_server_setup(live_server, tmpdir_factory): relay_port = ephemeral_port_reserve.reserve(ip="127.0.0.1", 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 +81,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 +107,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"}}, diff --git a/src/sentry/testutils/skips.py b/src/sentry/testutils/skips.py index 61f2ae758c215e..58c8a98857f995 100644 --- a/src/sentry/testutils/skips.py +++ b/src/sentry/testutils/skips.py @@ -1,10 +1,8 @@ from __future__ import annotations import socket -from urllib.parse import urlparse import pytest -from django.conf import settings def _service_available(host: str, port: int) -> bool: @@ -23,38 +21,30 @@ def _requires_service_message(name: str) -> str: @pytest.fixture(scope="session") def _requires_snuba() -> None: - parsed = urlparse(settings.SENTRY_SNUBA) - assert parsed.hostname is not None - assert parsed.port is not None - if not _service_available(parsed.hostname, parsed.port): + # TODO: ability to ask devservices what port a service is on + if not _service_available("127.0.0.1", 1218): pytest.fail(_requires_service_message("snuba")) @pytest.fixture(scope="session") def _requires_kafka() -> None: - kafka_conf = settings.SENTRY_DEVSERVICES["kafka"](settings, {}) - (port,) = kafka_conf["ports"].values() - - if not _service_available("127.0.0.1", port): + # TODO: ability to ask devservices what port a service is on + if not _service_available("127.0.0.1", 9092): pytest.fail(_requires_service_message("kafka")) @pytest.fixture(scope="session") def _requires_symbolicator() -> None: - symbolicator_conf = settings.SENTRY_DEVSERVICES["symbolicator"](settings, {}) - (port,) = symbolicator_conf["ports"].values() - - if not _service_available("127.0.0.1", port): + # TODO: ability to ask devservices what port a service is on + if not _service_available("127.0.0.1", 3021): service_message = "requires 'symbolicator' server running\n\t💡 Hint: run `devservices up --mode=symbolicator`" pytest.fail(service_message) @pytest.fixture(scope="session") def _requires_objectstore() -> None: - objectstore_conf = settings.SENTRY_DEVSERVICES["objectstore"](settings, {}) - (port,) = objectstore_conf["ports"].values() - - if not _service_available("127.0.0.1", port): + # TODO: ability to ask devservices what port a service is on + if not _service_available("127.0.0.1", 8888): service_message = "requires 'objectstore' server running\n\t💡 Hint: run `devservices up --mode=objectstore`" pytest.skip(service_message)