Skip to content

Commit

Permalink
feat: Implementation of reverse proxies for `varfish-docker-compose-n…
Browse files Browse the repository at this point in the history
…g` (#10) (#12)
  • Loading branch information
gromdimon authored Aug 22, 2023
1 parent 9cf42a4 commit c3884a4
Show file tree
Hide file tree
Showing 8 changed files with 728 additions and 314 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
- name: Set up node 20.x
uses: actions/setup-node@v3
with:
noe-version: "20.x"
node-version: "20.x"
cache: "npm"
cache-dependency-path: |
frontend/package-lock.json
Expand All @@ -110,7 +110,7 @@ jobs:
- name: Set up node 20.x
uses: actions/setup-node@v3
with:
noe-version: "20.x"
node-version: "20.x"
cache: "npm"
cache-dependency-path: |
frontend/package-lock.json
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.coverage
coverage.lcov

# Environment variables
.env

# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,pycharm,vim,emacs
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,pycharm,vim,emacs

Expand Down
7 changes: 6 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ format-black:
lint: \
lint-isort \
lint-black \
lint-flake8
lint-flake8 \
lint-mypy

.PHONY: lint-isort
lint-isort:
Expand All @@ -49,6 +50,10 @@ lint-black:
flake8:
pipenv run flake8 --max-line-length 100 $(DIRS_PYTHON)

.PHONY: lint-mypy
lint-mypy:
pipenv run mypy $(DIRS_PYTHON)

.PHONY: test
test:
pipenv run pytest \
Expand Down
7 changes: 6 additions & 1 deletion backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ name = "pypi"
fastapi = "*"
pydantic = "*"
uvicorn = "*"
python-dotenv = "*"
httpx = "*"
requests-mock = "*"

[dev-packages]
black = "*"
flake8 = "*"
isort = "*"
mypy = "*"
pytest = "*"
pytest-asyncio = "*"
pytest-cov = "*"
pytest-httpx = "*"
sphinx = "*"
sphinx-rtd-theme = "*"
starlette = "*"
httpx = "*"

[requires]
python_version = "3.10"
845 changes: 561 additions & 284 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

82 changes: 74 additions & 8 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,92 @@
import os
import sys

from fastapi import FastAPI
import httpx
from dotenv import load_dotenv
from fastapi import FastAPI, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.responses import RedirectResponse
from starlette.background import BackgroundTask
from starlette.requests import Request
from starlette.responses import JSONResponse, RedirectResponse, Response, StreamingResponse

# Load environment
env = os.environ
load_dotenv()

#: Path to frontend build, if any.
SERVE_FRONTEND = os.environ.get("REEV_SERVE_FRONTEND")
SERVE_FRONTEND = env.get("REEV_SERVE_FRONTEND")
#: Debug mode
DEBUG = env.get("REEV_DEBUG", "false").lower() in ("true", "1")
#: Prefix for the backend of annonars service
BACKEND_PREFIX_ANNONARS = env.get("REEV_BACKEND_PREFIX_ANNONARS", "http://annonars")
#: Prefix for the backend of mehari service
BACKEND_PREFIX_MEHARI = env.get("REEV_BACKEND_PREFIX_MEHARI", "http://mehari")
#: Prefix for the backend of viguno service
BACKEND_PREFIX_VIGUNO = env.get("REEV_BACKEND_PREFIX_VIGUNO", "http://viguno")


app = FastAPI()

# Configure CORS settings
origins = [
"http://localhost", # Update with the actual frontend URL
"http://localhost:8081", # Update with the actual frontend URL
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Reverse proxy implementation
client = httpx.AsyncClient()


async def reverse_proxy(request: Request) -> Response:
"""Implement reverse proxy for backend services."""
url = request.url
backend_url = None

if url.path.startswith("/proxy/annonars"):
backend_url = BACKEND_PREFIX_ANNONARS + url.path.replace("/proxy/annonars", "/annos")
elif url.path.startswith("/proxy/mehari"):
backend_url = BACKEND_PREFIX_MEHARI + url.path.replace("/proxy/mehari", "")
elif url.path.startswith("/proxy/viguno"):
backend_url = BACKEND_PREFIX_VIGUNO + url.path.replace("/proxy/viguno", "")

if backend_url:
backend_url = backend_url + (f"?{url.query}" if url.query else "")
backend_req = client.build_request(
method=request.method,
url=backend_url,
headers=request.headers.raw,
content=await request.body(),
)
backend_resp = await client.send(backend_req, stream=True)
return StreamingResponse(
backend_resp.aiter_raw(),
status_code=backend_resp.status_code,
headers=backend_resp.headers,
background=BackgroundTask(backend_resp.aclose),
)
else:
return Response(status_code=404, content="Reverse proxy route not found")


@app.get("/api/hello")
def read_root():
return {"Hello": "World"}
# Register reverse proxy route
app.add_route("/proxy/{path:path}", reverse_proxy, methods=["GET", "POST"])


if SERVE_FRONTEND:
# Routes
if SERVE_FRONTEND: # pragma: no cover
print(f"SERVE_FRONTEND = {SERVE_FRONTEND}", file=sys.stderr)
app.mount("/ui", StaticFiles(directory=SERVE_FRONTEND), name="app")

@app.get("/")
async def redirect():
response = RedirectResponse(url=f"/ui/index.html")
response = RedirectResponse(url="/ui/index.html")
return response
18 changes: 0 additions & 18 deletions backend/tests/test_foo.py

This file was deleted.

76 changes: 76 additions & 0 deletions backend/tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import typing

import pytest
from app import main
from requests_mock import Mocker
from starlette.testclient import TestClient

#: Host name to use for the mocked backend.
MOCKED_BACKEND_HOST = "mocked-backend"

#: A "token" to be used in test URLs, does not carry a meaning as it is mocked.
MOCKED_URL_TOKEN = "xXTeStXxx"

#: FastAPI/startlette test client.
client = TestClient(main.app)


@pytest.fixture
def non_mocked_hosts() -> typing.List[str]:
"""List of hosts that should not be mocked.
We read the host from ``client``.
"""
return [client._base_url.host]


@pytest.mark.asyncio
async def test_proxy_annonars(monkeypatch, httpx_mock):
"""Test proxying to annonars backend."""
monkeypatch.setattr(main, "BACKEND_PREFIX_ANNONARS", f"http://{MOCKED_BACKEND_HOST}")
httpx_mock.add_response(
url=f"http://{MOCKED_BACKEND_HOST}/annos/{MOCKED_URL_TOKEN}",
method="GET",
text="Mocked response",
)

response = client.get(f"/proxy/annonars/{MOCKED_URL_TOKEN}")
assert response.status_code == 200
assert response.text == "Mocked response"


@pytest.mark.asyncio
async def test_proxy_mehari(monkeypatch, httpx_mock):
"""Test proxying to mehari backend."""
monkeypatch.setattr(main, "BACKEND_PREFIX_MEHARI", f"http://{MOCKED_BACKEND_HOST}")
httpx_mock.add_response(
url=f"http://{MOCKED_BACKEND_HOST}/{MOCKED_URL_TOKEN}",
method="GET",
text="Mocked response",
)

response = client.get(f"/proxy/mehari/{MOCKED_URL_TOKEN}")
assert response.status_code == 200
assert response.text == "Mocked response"


@pytest.mark.asyncio
async def test_proxy_viguno(monkeypatch, httpx_mock):
"""Test proxying to viguno backend."""
monkeypatch.setattr(main, "BACKEND_PREFIX_VIGUNO", f"http://{MOCKED_BACKEND_HOST}")
httpx_mock.add_response(
url=f"http://{MOCKED_BACKEND_HOST}/{MOCKED_URL_TOKEN}",
method="GET",
text="Mocked response",
)

response = client.get(f"/proxy/viguno/{MOCKED_URL_TOKEN}")
assert response.status_code == 200
assert response.text == "Mocked response"


@pytest.mark.asyncio
async def test_invalid_proxy_route(monkeypatch, httpx_mock):
"""Test invalid proxy route."""
response = client.get("/proxy/some-other-path")
assert response.status_code == 404
assert response.text == "Reverse proxy route not found"

0 comments on commit c3884a4

Please sign in to comment.