From d23b29cf2a41430667a7a3d832a0a05d28961f7f Mon Sep 17 00:00:00 2001 From: Ben Avrahami Date: Mon, 14 Feb 2022 15:58:43 +0200 Subject: [PATCH] added doc mode + added image tests + added docs about doc mode (#47) --- CHANGELOG.md | 3 + Dockerfile | 15 ++- docs/api.rst | 20 +++- docs/concepts.rst | 2 +- docs/running.rst | 19 ++- docs/setting_versions.rst | 5 +- heksher/app.py | 28 ++++- heksher/main.py | 2 + poetry.lock | 178 +++-------------------------- pyproject.toml | 4 +- readme.md | 61 +--------- tests/blackbox/app/conftest.py | 24 ---- tests/blackbox/conftest.py | 37 ++++-- tests/blackbox/image/test_image.py | 107 ++++++++++++----- 14 files changed, 204 insertions(+), 301 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b081a9d..c7386e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * The api endpoint PUT /api/v1/rules//value to change a rule's value * The api endpoint GET /api/v1/query to query rules (replaces the old query endpoint) * POST /api/v1/rules now returns the rule location in the header +* added DOC_ONLY mode, read more about in the documentation ### Fixed * A bug where patching a context feature's index using "to_before" would use the incorrect target. ### Internal @@ -27,6 +28,8 @@ * tools/mk_revision.py to easily create alembic revisions * all db logic refactored to avoid multiple connections * Many more column are now strictly non-nullable +* async-asgi-testclient is now a dev-dependency. +* added proper image tests ## 0.4.1 ### Removed * removed the alembic extra, it's now a requirement diff --git a/Dockerfile b/Dockerfile index 8c6ce99..cca825d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.8 +FROM python:3.8 RUN apt-get update && \ apt-get -y install gcc build-essential @@ -7,11 +7,10 @@ RUN mkdir -p /usr/src/app/heksher WORKDIR /usr/src/app/heksher -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \ - cd /usr/local/bin && \ - ln -s /opt/poetry/bin/poetry && \ - poetry config virtualenvs.create false -COPY pyproject.toml poetry.lock* ./ +RUN pip install pip --upgrade +RUN pip install poetry +RUN poetry config virtualenvs.create false +COPY pyproject.toml poetry.lock ./ RUN poetry run pip install --upgrade pip RUN poetry install --no-dev --no-root @@ -21,5 +20,5 @@ RUN export APP_VERSION=$(poetry version | cut -d' ' -f2) && echo "__version__ = ENV PYTHONPATH=${PYTHONPATH}:/usr/src/app/heksher ENV PYTHONOPTIMIZE=1 -ENV WEB_CONCURRENCY=1 -ENV MODULE_NAME=heksher.main + +CMD ["uvicorn", "heksher.main:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/docs/api.rst b/docs/api.rst index 73d1316..ca509bc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -12,6 +12,16 @@ Since Heksher is a FastAPI service, the API can also be accessed via the redoc e The most common endpoints for users are :ref:`setting declaration `, and :ref:`querying ` +.. note:: + + You can view the api in a swagger or redoc-style format locally by running heksher in :ref:`running:Doc Only Mode` + + .. code-block:: console + + docker run -d -p 9999:80 --name heksher-doc-only -e DOC_ONLY=true biocatchltd/heksher + + and accessing http://localhost:9999/redoc or http://localhost:9999/docs + General ------- @@ -399,8 +409,8 @@ Response: * **metadata**: A dictionary of metadata associated with the setting. Only included if include_additional_data is true. * **aliases**: A list aliases of the setting. Only included if include_additional_data is true. -PUT /api/v1/settings//type -******************************** +PUT /api/v1/settings//type +******************************************* Change a setting's type in a way that is not necessarily backwards compatible. @@ -416,8 +426,8 @@ If there are type conflicts, the 409 response will have the schema: * **conflicts**: A list of strings describing the conflicts. -PUT /api/v1/settings//name -********************************* +PUT /api/v1/settings//name +********************************************* Rename a setting. @@ -432,7 +442,7 @@ alias to the setting and an empty 204 response will be returned. If the new name is already in use, or if the version is incompatible with the latest declaration, a 409 response will be returned. -PUT /api/v1/settings/setting_name>/configurable_features +PUT /api/v1/settings//configurable_features *********************************************************** Change the configurable features of a setting. diff --git a/docs/concepts.rst b/docs/concepts.rst index 794c8cf..95133df 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -14,7 +14,7 @@ attributes: * metadata: additional metadata about the setting. * aliases: a list of :ref:`aliases ` of the setting. * default: the default value of the setting. -* version: the latest version of the setting declaration, see :ref:`setting_versioning:Setting Versioning`. +* version: the latest version of the setting declaration, see :ref:`setting_versions:Setting Versions`. Context Features ----------------------- diff --git a/docs/running.rst b/docs/running.rst index 101b9a2..3bea7f8 100644 --- a/docs/running.rst +++ b/docs/running.rst @@ -6,7 +6,7 @@ Heksher is an HTTP service, running it is best done through the .. code-block:: console - docker run -d -p 80:80 --name heksher biocatchltd/heksher -E ... + docker run -d -p 80:80 --name heksher -e ... biocatchltd/heksher Dependencies ----------------- @@ -15,7 +15,7 @@ environment variable ``HEKSHER_DB_CONNECTION_STRING`` as a driverless sqlalchemy .. code-block:: console - docker run -d -p 80:80 --name heksher biocatchltd/heksher -E HEKSHER_DB_CONNECTION_STRING=postgresql://user:password@host:port/dbname -E ... + docker run -d -p 80:80 --name heksher -e HEKSHER_DB_CONNECTION_STRING=postgresql://user:password@host:port/dbname -e ... biocatchltd/heksher The database must be initialized to Heksher's schema. the database's schema is handled with `alembic `_. For convenience, the database can be initialized with @@ -23,7 +23,7 @@ alembic using the Heksher image. .. code-block:: console - docker run biocatchltd/heksher alembic upgrade head -E HEKSHER_DB_CONNECTION_STRING=postgresql://user:password@host:port/dbname + docker run -e HEKSHER_DB_CONNECTION_STRING=postgresql://user:password@host:port/dbname biocatchltd/heksher alembic upgrade head .. note:: @@ -43,4 +43,15 @@ The following environment variables are optional for logging: * **HEKSHER_LOGSTASH_HOST**: the logstash host to send logs to. * **HEKSHER_LOGSTASH_PORT**: the logstash port to send logs to. * **HEKSHER_LOGSTASH_LEVEL**: the log level to send logs on. -* **HEKSHER_LOGSTASH_TAGS**: additional tags to send with logs. \ No newline at end of file +* **HEKSHER_LOGSTASH_TAGS**: additional tags to send with logs. + +Doc Only Mode +------------------------ + +Heksher also has a doc-only mode, where all routes and endpoints are disabled, except fastapi's standard doc pages: +``/redoc`` and ``/docs``. This mode can enabled by passing the environment variable ``DOC_ONLY=true``. If doc-mode is +enabled, no connection to any underlying dependency is made, and the service will start up even if all other environment +variables are missing. + +When in doc-only mode, attempting to access any api in heksher other than those above (and ``/api/health``) will result +in a 500 error code. \ No newline at end of file diff --git a/docs/setting_versions.rst b/docs/setting_versions.rst index 0e99d76..7cbfb68 100644 --- a/docs/setting_versions.rst +++ b/docs/setting_versions.rst @@ -14,6 +14,7 @@ the latest declared version of the setting. latest declaration. If the assertion fails, we inform the user of an attribute mismatch. * If the latest declared version is higher than the declaration version, we inform the user that they are declaring with outdated attributes. + .. warning:: Differing attributes are not checked for older versions. If a user purposely declares a setting with an older @@ -60,5 +61,5 @@ might fail depending on the state of the ruleset of the service. If these confli API, our app might fail. To avoid this, these potentially conflicting changes can be made explicit with explicit API calls. These API endpoints are: -* :ref:`PUT /api/v1/settings/setting_name>/configurable_features` -* :ref:`PUT /api/v1/settings/setting_name>/type` \ No newline at end of file +* :ref:`api:PUT /api/v1/settings//configurable_features` +* :ref:`api:PUT /api/v1/settings//type` \ No newline at end of file diff --git a/heksher/app.py b/heksher/app.py index c0409bf..aa53f58 100644 --- a/heksher/app.py +++ b/heksher/app.py @@ -8,8 +8,10 @@ from aiologstash import create_tcp_handler from envolved import EnvVar, Schema from envolved.parsers import CollectionParser -from fastapi import FastAPI +from fastapi import FastAPI, Request from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine +from starlette.responses import PlainTextResponse +from starlette.status import HTTP_404_NOT_FOUND from heksher._version import __version__ from heksher.db_logic.context_feature import db_add_context_features, db_get_context_features, db_move_context_features @@ -21,6 +23,7 @@ connection_string = EnvVar('HEKSHER_DB_CONNECTION_STRING', type=db_url_with_async_driver) startup_context_features = EnvVar('HEKSHER_STARTUP_CONTEXT_FEATURES', type=CollectionParser(';', str), default=None) +doc_only_ev = EnvVar("DOC_ONLY", type=bool, default=False) class LogstashSettingSchema(Schema): @@ -34,6 +37,14 @@ class LogstashSettingSchema(Schema): logstash_settings_ev = EnvVar('HEKSHER_LOGSTASH_', default=None, type=LogstashSettingSchema) sentry_dsn_ev = EnvVar('SENTRY_DSN', default='', type=str) +redoc_mode_whitelist = frozenset(( + '/favicon.ico', + '/docs', + '/redoc', + '/openapi.json', + '/api/health', +)) + class HeksherApp(FastAPI): """ @@ -41,6 +52,7 @@ class HeksherApp(FastAPI): """ engine: AsyncEngine health_monitor: HealthMonitor + doc_only: bool async def ensure_context_features(self, expected_context_features: Sequence[str]): async with self.engine.connect() as conn: @@ -64,6 +76,20 @@ async def ensure_context_features(self, expected_context_features: Sequence[str] await db_add_context_features(conn, dict(super_sequence)) async def startup(self): + self.doc_only = doc_only_ev.get() + if self.doc_only: + @self.middleware('http') + async def doc_only_middleware(request: Request, call_next): + path = request.url.path + if path.endswith("/"): + # our whitelist is without a trailing slash. we remove the slash here and let starlette's redirect + # do its work + path = path[:-1] + if path not in redoc_mode_whitelist: + return PlainTextResponse("The server is running in doc_only mode, only docs/ and redoc/ paths" + " are supported", HTTP_404_NOT_FOUND) + return await call_next(request) + return logstash_settings = logstash_settings_ev.get() if logstash_settings is not None: handler = await create_tcp_handler(logstash_settings.host, logstash_settings.port, extra={ diff --git a/heksher/main.py b/heksher/main.py index 5a6850a..50dce3e 100644 --- a/heksher/main.py +++ b/heksher/main.py @@ -24,6 +24,8 @@ async def health_check(): """ Check the health of the connections to the service """ + if app.doc_only: + return JSONResponse({'version': __version__, 'doc_only': True}) if not app.health_monitor.status: return JSONResponse({'version': __version__}, status_code=500) return {'version': __version__} diff --git a/poetry.lock b/poetry.lock index 361b963..e4de954 100644 --- a/poetry.lock +++ b/poetry.lock @@ -39,7 +39,7 @@ tz = ["python-dateutil"] name = "async-asgi-testclient" version = "1.4.7" description = "Async client for testing ASGI web applications" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -116,7 +116,7 @@ python-versions = "*" name = "charset-normalizer" version = "2.0.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" +category = "dev" optional = false python-versions = ">=3.5.0" @@ -349,23 +349,6 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] -[[package]] -name = "jsonschema" -version = "4.2.1" -description = "An implementation of JSON Schema validation for Python" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -attrs = ">=17.4.0" -importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} -pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format_nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] - [[package]] name = "mako" version = "1.1.6" @@ -401,7 +384,7 @@ python-versions = "*" name = "multidict" version = "5.2.0" description = "multidict implementation" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -477,17 +460,6 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -[[package]] -name = "pyaml" -version = "21.10.1" -description = "PyYAML-based module to produce pretty and readable YAML-serialized data" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -PyYAML = "*" - [[package]] name = "pycodestyle" version = "2.7.0" @@ -538,14 +510,6 @@ python-versions = ">=3.6" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] -[[package]] -name = "pyrsistent" -version = "0.18.0" -description = "Persistent/Functional/Immutable data structures" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "pytest" version = "6.2.5" @@ -569,17 +533,17 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-asyncio" -version = "0.16.0" -description = "Pytest support for asyncio." +version = "0.18.1" +description = "Pytest support for asyncio" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.7" [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=6.1.0" [package.extras] -testing = ["coverage", "hypothesis (>=5.7.1)"] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)"] [[package]] name = "pytest-cov" @@ -621,19 +585,11 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "pyyaml" -version = "6.0" -description = "YAML parser and emitter for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "requests" version = "2.26.0" description = "Python HTTP for Humans." -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" @@ -690,14 +646,6 @@ sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] tornado = ["tornado (>=5)"] -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "sniffio" version = "1.2.0" @@ -819,21 +767,6 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] -[[package]] -name = "sphinxcontrib-redoc" -version = "1.6.0" -description = "ReDoc powered OpenAPI (fka Swagger) spec renderer for Sphinx" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -jinja2 = ">=2.4" -jsonschema = ">=3.0" -PyYAML = ">=3.12" -six = ">=1.5" -sphinx = ">=1.5" - [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" @@ -963,7 +896,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "yellowbox" -version = "0.6.8" +version = "0.6.9" description = "" category = "dev" optional = false @@ -971,20 +904,20 @@ python-versions = ">=3.7,<4.0" [package.dependencies] docker = ">=4.2.0,<6.0.0" -psycopg2 = {version = ">=2.8.6,<3.0.0", optional = true, markers = "extra == \"postgresql\" or extra == \"_all\""} +psycopg2 = {version = ">=2.8.6,<3.0.0", optional = true, markers = "extra == \"postgresql\" or extra == \"dev\""} requests = "*" -sqlalchemy = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"postgresql\" or extra == \"_all\""} +sqlalchemy = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"postgresql\" or extra == \"dev\""} yaspin = ">=1.0.0,<2.0.0" [package.extras] redis = ["redis (>=3.3.0)"] -_all = ["redis (>=3.3.0)", "pika", "kafka-python", "azure-storage-blob (>=12.0.0,<12.7.0)", "cffi (>=1.14.0,<2.0.0)", "sqlalchemy (>=1.3.0,<2.0.0)", "psycopg2 (>=2.8.6,<3.0.0)", "simple-websocket-server", "python-igraph (>=0.9.6,!=0.9.8,<0.10.0)", "starlette", "uvicorn", "websockets", "hvac"] +dev = ["redis (>=3.3.0)", "pika", "kafka-python", "azure-storage-blob (>=12.0.0,<12.7.0)", "cffi (>=1.14.0,<2.0.0)", "sqlalchemy (>=1.3.0,<2.0.0)", "psycopg2 (>=2.8.6,<3.0.0)", "simple-websocket-server", "python-igraph (==0.9.7)", "starlette (>=0.9.0)", "uvicorn (>=0.13.0)", "websockets", "hvac"] rabbit = ["pika"] kafka = ["kafka-python"] azure = ["azure-storage-blob (>=12.0.0,<12.7.0)", "cffi (>=1.14.0,<2.0.0)"] postgresql = ["sqlalchemy (>=1.3.0,<2.0.0)", "psycopg2 (>=2.8.6,<3.0.0)"] websocket = ["simple-websocket-server"] -webserver = ["python-igraph (>=0.9.6,!=0.9.8,<0.10.0)", "starlette", "uvicorn", "websockets"] +webserver = ["python-igraph (==0.9.7)", "starlette (>=0.9.0)", "uvicorn (>=0.13.0)", "websockets"] vault = ["hvac"] [[package]] @@ -1002,7 +935,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "~3.8" -content-hash = "638246e7a016838f0683da5d3d01af0be72b8f565dd456fc51bb0753618766ce" +content-hash = "ce1a65f7bd90248b9ccdbf48156a77bcc7ed62c53b8b5d7ecf63e81eb25a3668" [metadata.files] aiologstash = [ @@ -1230,10 +1163,6 @@ jinja2 = [ {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, ] -jsonschema = [ - {file = "jsonschema-4.2.1-py3-none-any.whl", hash = "sha256:2a0f162822a64d95287990481b45d82f096e99721c86534f48201b64ebca6e8c"}, - {file = "jsonschema-4.2.1.tar.gz", hash = "sha256:390713469ae64b8a58698bb3cbc3859abe6925b565a973f87323ef21b09a27a8"}, -] mako = [ {file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"}, {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, @@ -1449,10 +1378,6 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -pyaml = [ - {file = "pyaml-21.10.1-py2.py3-none-any.whl", hash = "sha256:19985ed303c3a985de4cf8fd329b6d0a5a5b5c9035ea240eccc709ebacbaf4a0"}, - {file = "pyaml-21.10.1.tar.gz", hash = "sha256:c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383"}, -] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -1493,36 +1418,13 @@ pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] -pyrsistent = [ - {file = "pyrsistent-0.18.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-win32.whl", hash = "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1"}, - {file = "pyrsistent-0.18.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-win32.whl", hash = "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f"}, - {file = "pyrsistent-0.18.0-cp37-cp37m-win_amd64.whl", hash = "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2"}, - {file = "pyrsistent-0.18.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427"}, - {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef"}, - {file = "pyrsistent-0.18.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c"}, - {file = "pyrsistent-0.18.0-cp38-cp38-win32.whl", hash = "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78"}, - {file = "pyrsistent-0.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b"}, - {file = "pyrsistent-0.18.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4"}, - {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680"}, - {file = "pyrsistent-0.18.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426"}, - {file = "pyrsistent-0.18.0-cp39-cp39-win32.whl", hash = "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b"}, - {file = "pyrsistent-0.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea"}, - {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, -] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, - {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, + {file = "pytest-asyncio-0.18.1.tar.gz", hash = "sha256:c43fcdfea2335dd82ffe0f2774e40285ddfea78a8e81e56118d47b6a90fbb09e"}, + {file = "pytest_asyncio-0.18.1-py3-none-any.whl", hash = "sha256:c9ec48e8bbf5cc62755e18c4d8bc6907843ec9c5f4ac8f61464093baeba24a7e"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, @@ -1549,41 +1451,6 @@ pywin32 = [ {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, ] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, @@ -1596,10 +1463,6 @@ sentry-sdk = [ {file = "sentry-sdk-1.5.0.tar.gz", hash = "sha256:789a11a87ca02491896e121efdd64e8fd93327b69e8f2f7d42f03e2569648e88"}, {file = "sentry_sdk-1.5.0-py2.py3-none-any.whl", hash = "sha256:0db297ab32e095705c20f742c3a5dac62fe15c4318681884053d0898e5abb2f6"}, ] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] sniffio = [ {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, @@ -1636,9 +1499,6 @@ sphinxcontrib-qthelp = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, ] -sphinxcontrib-redoc = [ - {file = "sphinxcontrib-redoc-1.6.0.tar.gz", hash = "sha256:e358edbe23927d36432dde748e978cf897283a331a03e93d3ef02e348dee4561"}, -] sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, @@ -1714,8 +1574,8 @@ yaspin = [ {file = "yaspin-1.5.0.tar.gz", hash = "sha256:d8fc9bc1c8be225877ea6e2e08fec96c2b52e233525a5c40b92d373f015439c6"}, ] yellowbox = [ - {file = "yellowbox-0.6.8-py3-none-any.whl", hash = "sha256:3a561b08081b05657958b2ce694ce80c28cd027a48946566835241abaa228e3b"}, - {file = "yellowbox-0.6.8.tar.gz", hash = "sha256:42663410f7a2f2e7ecfcafb4b2d5216f4ca102dc1ac104bcf7fbf7c0fd73701a"}, + {file = "yellowbox-0.6.9-py3-none-any.whl", hash = "sha256:907de4662026f2f0a269a6c093f1b78be36bca9865f49aa128c0d3fd58964011"}, + {file = "yellowbox-0.6.9.tar.gz", hash = "sha256:6ec3d329ec6348791146e368b0be857b71b2f5955f569c91fbfa5b82b2f64ae1"}, ] zipp = [ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, diff --git a/pyproject.toml b/pyproject.toml index 83bc0a2..3452235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,13 +18,12 @@ SQLAlchemy = "^1.4.21" asyncpg = "^0.23.0" aiologstash = "^2.0.0" sentry-sdk = "^1.1.0" -async-asgi-testclient = "^1.4.6" alembic = "^1.4.3" psycopg2 = "^2.9.2" [tool.poetry.dev-dependencies] -pytest-asyncio = "*" +async-asgi-testclient = "^1.4.6" pytest-cov = "^2.10" flake8 = "^3.8.4" requests = "^2.25.1" @@ -34,6 +33,7 @@ types-orjson = "^0.1.1" isort = "^5.9.1" Sphinx = "^4.3.1" sphinx-rtd-theme = "^1.0.0" +pytest-asyncio = "^0.18.1" [build-system] requires = ["poetry>=1.0.0"] diff --git a/readme.md b/readme.md index d08ec0a..d78a30c 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,8 @@ Currently, Heksher supports the following environment variables: database's existing context features to this list (or raise an error if it cannot). * `HEKSHER_LOGSTASH_HOST`, `HEKSHER_LOGSTASH_PORT`, `HEKSHER_LOGSTASH_LEVEL`, `HEKSHER_LOGSTASH_TAGS`: Optional values to allow sending logs to a logstash server. +* `DOC_ONLY`: set to "true" to enable doc-only mode, where the service does not actually connect to any sql database +and only the "/redoc" and "/doc" apis function. read more about DOC_ONLY mode in the documentation. The service itself can be run from the docker image [found in dockerhub](https://hub.docker.com/repository/docker/biocatchltd/heksher). @@ -53,62 +55,3 @@ The service doesn't provide any authorization/authentication as a feature. This Heksher supports an HTTP interface. There are many methods that adhere to be REST-ful (and can be viewed in full by accessing the `/redoc` route), but the two central routes that are not REST-ful are: -### PUT `/api/v1/settings/declare` -declare that a setting exists, and create it if it doesn't. - -arguments: -* name: the name of the setting. -* configurable_features: the names of the context features that are allowed to configure this setting (list[str]). -* type: the type of the setting. -* default_value: optional. default value of the setting. -* metadata: optional. additional metadata. - -output: -* created: bool, whether the setting was created or it already existed. -* rewritten: list of strings, containing whichever elements of any previous declaration was overwritten (if the setting - did not exist before the call, this list is empty) -* incomplete: a mapping of fields that were declared with incomplete data, with the complete data (which remains - unchanged) - -### GET `/api/v1/rules/query` -Get a set of relevant rules. - -arguments: -* setting_names: a list of setting names to retrieve (List[str]) -* context_features_options: a mapping of context features to lists of values, storing whichever context feature values to return (dict[str,List[str]]). -* cache_time: optional. If provided, all settings that have been unedited since this time are ignored. -* include_metadata: optional boolean (default False). If true, all rules are also provided with their metadata. - -output: -* rules: A dict, mapping each setting to a list of dicts, each dict representing a rule, having the keys: - * value - * context_features - * metadata. only present if “include_metadata” is true. - -Clients should use this route at regular intervals, updating the rules for each service, to be queried in real-time from -within the client's memory. Clients are encourage to store the rules in a nested mapping, the rules for `cache_size` in -the above example will be stored as: - -```json -{ - "john": { - "*": { - "*":100 - } - }, - "jim": { - "admin": { - "*": 200 - }, - "*": { - "*": 50 - } - }, - "*": { - "guest": { - "dark": 20, - "*": 10 - } - } -} -``` diff --git a/tests/blackbox/app/conftest.py b/tests/blackbox/app/conftest.py index 2a49474..f017e74 100644 --- a/tests/blackbox/app/conftest.py +++ b/tests/blackbox/app/conftest.py @@ -1,39 +1,15 @@ import asyncio import json -import sys -from subprocess import run from async_asgi_testclient import TestClient from pytest import fixture from sqlalchemy import select from sqlalchemy.ext.asyncio import create_async_engine -from yellowbox.extras.postgresql import PostgreSQLService from heksher.db_logic.metadata import context_features from heksher.main import app -@fixture(scope='session') -def sql_service(docker_client): - service: PostgreSQLService - with PostgreSQLService.run(docker_client) as service: - yield service - - -@fixture -def purge_sql(sql_service): - with sql_service.connection() as connection: - connection.execute(f''' - DROP SCHEMA public CASCADE; - CREATE SCHEMA public; - GRANT ALL ON SCHEMA public TO {sql_service.user}; - GRANT ALL ON SCHEMA public TO public; - ''') - # we run create_all from outside to avoid alembic's side effects - run([sys.executable, 'alembic/from_scratch.py', sql_service.local_connection_string()], check=True) - yield - - @fixture async def check_indexes_of_cf(sql_service): yield diff --git a/tests/blackbox/conftest.py b/tests/blackbox/conftest.py index 3493079..21a6066 100644 --- a/tests/blackbox/conftest.py +++ b/tests/blackbox/conftest.py @@ -1,14 +1,33 @@ -from docker import DockerClient +import sys +from subprocess import run + from pytest import fixture +from yellowbox import docker_client as _docker_client +from yellowbox.extras.postgresql import PostgreSQLService @fixture(scope='session') def docker_client(): - # todo improve when yellowbox is upgraded - try: - ret = DockerClient.from_env() - ret.ping() - except Exception: - return DockerClient(base_url='tcp://localhost:2375') - else: - return ret + with _docker_client() as dc: + yield dc + + +@fixture(scope='session') +def sql_service(docker_client): + service: PostgreSQLService + with PostgreSQLService.run(docker_client) as service: + yield service + + +@fixture +def purge_sql(sql_service): + with sql_service.connection() as connection: + connection.execute(f''' + DROP SCHEMA public CASCADE; + CREATE SCHEMA public; + GRANT ALL ON SCHEMA public TO {sql_service.user}; + GRANT ALL ON SCHEMA public TO public; + ''') + # we run create_all from outside to avoid alembic's side effects + run([sys.executable, 'alembic/from_scratch.py', sql_service.local_connection_string()], check=True) + yield diff --git a/tests/blackbox/image/test_image.py b/tests/blackbox/image/test_image.py index f5bfd30..3cf80ca 100644 --- a/tests/blackbox/image/test_image.py +++ b/tests/blackbox/image/test_image.py @@ -1,36 +1,89 @@ -import json +import sys +from threading import Thread +from httpx import HTTPError, get +from pytest import fixture +from yellowbox.containers import get_ports, killing +from yellowbox.image_build import build_image +from yellowbox.retry import RetrySpec -def _iter_build_log(build_log): - """Iterate over lines of the build log""" - remaining = b"" - for chunk in build_log: - if b'\n' not in chunk: - remaining += chunk - continue - if remaining: - chunk = remaining + chunk - remaining = b"" - str_chunk = chunk.decode("utf-8") - lines = str_chunk.splitlines() +@fixture(scope="session") +def image(docker_client): + with build_image(docker_client, 'heksher', path='.') as image: + yield image - if not str_chunk.endswith('\n'): - remaining += lines.pop().encode("utf-8") - for line in lines: - obj = json.loads(line) - stream = obj.get("stream") - if stream: - yield stream.strip() +def test_image_builds(image): + pass -def test_image_builds(docker_client): - build_log = docker_client.api.build(path=".", tag='heksher:testing', rm=True) +def test_startup_healthy(image, docker_client, monkeypatch, sql_service, purge_sql): + env = { + 'HEKSHER_DB_CONNECTION_STRING': sql_service.host_connection_string(), + 'HEKSHER_STARTUP_CONTEXT_FEATURES': 'user;trust;theme', + } - # Wait till build is finished. - for line in _iter_build_log(build_log): - print(line) + with killing(docker_client.containers.create(image, environment=env, ports={80: 0})) as container: + container.start() + log_stream = container.logs(stream=True) - assert docker_client.images.get('heksher:testing') - docker_client.images.remove('heksher:testing', force=True, noprune=True) + def pipe(): + for line_b in log_stream: + line = str(line_b, 'utf-8').strip() + print(line, file=sys.stderr) + + pipe_thread = Thread(target=pipe) + pipe_thread.start() + + container.reload() + container_port = get_ports(container)[80] + + retry_spec = RetrySpec(0.5, 10) + + retry_spec.retry(lambda: get(f'http://localhost:{container_port}/api/health').raise_for_status(), HTTPError) + + response = get(f'http://localhost:{container_port}/api/health') + response.raise_for_status() + response = get(f'http://localhost:{container_port}/docs') + response.raise_for_status() + response = get(f'http://localhost:{container_port}/redoc') + response.raise_for_status() + response = get(f'http://localhost:{container_port}/api/v1/context_features') + response.raise_for_status() + container.remove(v=True) + + +def test_startup_doc_only_healthy(image, docker_client, monkeypatch): + env = { + 'DOC_ONLY': 'true' + } + + with killing(docker_client.containers.create(image, environment=env, ports={80: 0})) as container: + container.start() + log_stream = container.logs(stream=True) + + def pipe(): + for line_b in log_stream: + line = str(line_b, 'utf-8').strip() + print(line, file=sys.stderr) + + pipe_thread = Thread(target=pipe) + pipe_thread.start() + + container.reload() + container_port = get_ports(container)[80] + + retry_spec = RetrySpec(0.5, 10) + + retry_spec.retry(lambda: get(f'http://localhost:{container_port}/api/health').raise_for_status(), HTTPError) + + response = get(f'http://localhost:{container_port}/api/health') + response.raise_for_status() + response = get(f'http://localhost:{container_port}/docs') + response.raise_for_status() + response = get(f'http://localhost:{container_port}/redoc') + response.raise_for_status() + response = get(f'http://localhost:{container_port}/api/v1/context_features') + assert response.is_error + container.remove(v=True)