diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5bfb9bd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + +jobs: + check: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v1 + + - name: build + run: docker compose build check + + - name: check + run: docker compose run --rm check + + - name: test + run: docker compose run --rm check poetry run pytest tests/ diff --git a/README.md b/README.md index 96334e1..0ca5df2 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,33 @@ geOrchestra Maelstro is an application which helps synchronise geonetwork and ge Refer to documentation from https://github.com/georchestra/docker/tree/master?tab=readme-ov-file#on-linux to trust caddy certificate Also you need to run few commands before to start documented here : [georchestra/README.md](georchestra/README.md) + +## Backend + +The folder [backend](backend) contains the API written with FastAPI. + +### Access +In the global dev composition, the backend is accessible via the https gateway: +https://georchestra-127-0-0-1.nip.io/maelstro-backend/ + +### SwaggerUI + +FastAPI automatically builds a swagger API web interface which can be found at +https://georchestra-127-0-0-1.nip.io/maelstrob/docs + +Here the various API entrypoints can be tested + +## CI + +Automatic code quality checks are implemented in the CI. + +The code test can be launched manually via the docker command below. +``` +docker compose run --rm check +``` + +In case formatting issues are found, the code can be auto-fixed with: +``` +docker compose run --rm check /app/maelstro/scripts/code_fix.sh +``` diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..9414382 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/backend/Dockerfile b/backend/Dockerfile index 3ac3ab2..f4579f4 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,8 +1,28 @@ -FROM python:3.12-slim-bullseye +FROM python:3.12-slim-bullseye AS poetry -RUN pip install "fastapi[standard]" requests +RUN --mount=type=cache,target=/root/.cache \ + pip install poetry +RUN poetry config virtualenvs.create false RUN mkdir /app +WORKDIR /app + +COPY pyproject.toml /app + +FROM poetry as server +RUN --mount=type=cache,target=/root/.cache \ + poetry install --no-root + COPY . /app -WORKDIR /app +RUN --mount=type=cache,target=/root/.cache \ + poetry install + +CMD ["serve_prod"] + +FROM server as check + +RUN --mount=type=cache,target=/root/.cache \ + poetry install --no-root --with check + +CMD ["/app/maelstro/scripts/code_check.sh"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..86febc7 --- /dev/null +++ b/backend/README.md @@ -0,0 +1 @@ +Maelstro sync app for Georchestra diff --git a/backend/health_check.py b/backend/health_check.py deleted file mode 100755 index ebc0aeb..0000000 --- a/backend/health_check.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/local/bin/python - -import requests - -assert requests.get('http://localhost:8000/health').status_code == 200 diff --git a/backend/maelstro/__init__.py b/backend/maelstro/__init__.py new file mode 100644 index 0000000..39938b1 --- /dev/null +++ b/backend/maelstro/__init__.py @@ -0,0 +1,62 @@ +""" +Entry point scripts for maelstro backend server +""" + +import uvicorn +from fastapi_cli.utils.cli import get_uvicorn_log_config + + +def dev(): + """ + Dev server entrypoint: + special configuration: + - listens only on localhost + - restarts on code change + """ + uvicorn.run( + app="maelstro.main:app", + host="127.0.0.1", + port=8000, + reload=True, + workers=None, + root_path="", + # proxy_headers=proxy_headers, + log_config=get_uvicorn_log_config(), + ) + + +def docker_dev(): + """ + Dev server entrypoint for running the server inside a docker container: + special configuration: + - restarts on code change + """ + uvicorn.run( + app="maelstro.main:app", + host="0.0.0.0", + port=8000, + reload=True, + workers=None, + root_path="", + # proxy_headers=proxy_headers, + log_config=get_uvicorn_log_config(), + ) + + +def prod(): + """ + Server entrypoint for running the server inside a docker container: + """ + uvicorn.run( + app="maelstro.main:app", + host="0.0.0.0", + port=8000, + workers=None, # to be configured + root_path="", + # proxy_headers=proxy_headers, + log_config=get_uvicorn_log_config(), + ) + + +if __name__ == "__main__": + docker_dev() diff --git a/backend/maelstro/main.py b/backend/maelstro/main.py new file mode 100644 index 0000000..468c35f --- /dev/null +++ b/backend/maelstro/main.py @@ -0,0 +1,93 @@ +""" +Main backend app setup +""" + +from typing import Annotated +from fastapi import FastAPI, Request, Response, Header + + +app = FastAPI(root_path="/maelstro-backend") + +app.state.health_countdown = 5 + + +@app.head("/") +@app.get("/") +def root_page(): + """ + Hello world dummy route + """ + return {"Hello": "World"} + + +@app.get("/user") +def user_page( + sec_username: Annotated[str | None, Header(include_in_schema=False)] = None, + sec_org: Annotated[str | None, Header(include_in_schema=False)] = None, + sec_roles: Annotated[str | None, Header(include_in_schema=False)] = None, + sec_external_authentication: Annotated[ + str | None, Header(include_in_schema=False) + ] = None, + sec_proxy: Annotated[str | None, Header(include_in_schema=False)] = None, + sec_orgname: Annotated[str | None, Header(include_in_schema=False)] = None, +): + """ + Display user information provided by gateway + """ + return { + "username": sec_username, + "org": sec_org, + "roles": sec_roles, + "external-authentication": sec_external_authentication, + "proxy": sec_proxy, + "orgname": sec_orgname, + } + + +# pylint: disable=fixme +# TODO: deactivate for prod +@app.head("/debug") +@app.get("/debug") +@app.put("/debug") +@app.post("/debug") +@app.delete("/debug") +@app.options("/debug") +@app.patch("/debug") +def debug_page(request: Request): + """ + Display details of query including headers. + This may be useful in development to check all the headers provided by the gateway. + This entrypoint should be deactivated in prod. + """ + return { + **{ + k: request[k] + for k in [ + "type", + "asgi", + "http_version", + "server", + "client", + "scheme", + "root_path", + ] + }, + "method": request.method, + "url": request.url, + "headers": dict(request.headers), + "query_params": request.query_params.multi_items(), + } + + +@app.get("/health") +def health_check(response: Response): + """ + Health check to make sure the server is up and running + For test purposes, the server is reported healthy only from the 5th request onwards + """ + status: str = "healthy" + if app.state.health_countdown > 0: + app.state.health_countdown -= 1 + response.status_code = 404 + status = "unhealthy" + return {"status": status, "user": None} diff --git a/backend/maelstro/scripts/code_check.sh b/backend/maelstro/scripts/code_check.sh new file mode 100755 index 0000000..5bc7ca8 --- /dev/null +++ b/backend/maelstro/scripts/code_check.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd "$(dirname "$0")"/.. + +poetry run black --check . && \ +poetry run mypy . && \ +poetry run pyflakes . && \ +poetry run pylint . + +! (( $? & 7 )) # mask exit code for minor findings (refactor, convention, usage) diff --git a/backend/maelstro/scripts/code_fix.sh b/backend/maelstro/scripts/code_fix.sh new file mode 100755 index 0000000..c223dc4 --- /dev/null +++ b/backend/maelstro/scripts/code_fix.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cd "$(dirname "$0")"/.. + +poetry run black . diff --git a/backend/maelstro/scripts/health_check.py b/backend/maelstro/scripts/health_check.py new file mode 100755 index 0000000..8c1e30c --- /dev/null +++ b/backend/maelstro/scripts/health_check.py @@ -0,0 +1,7 @@ +#!/usr/local/bin/python + +import requests + + +def check(): + assert requests.get("http://localhost:8000/health").status_code == 200 diff --git a/backend/main.py b/backend/main.py deleted file mode 100644 index 5ddff2b..0000000 --- a/backend/main.py +++ /dev/null @@ -1,19 +0,0 @@ -from fastapi import FastAPI, Response - -app = FastAPI() - -app.state.health_countdown = 5 - -@app.get("/") -def root_page(): - return {"Hello": "World"} - - -@app.get("/health") -def health_check(response: Response): - status: str = "healthy" - if app.state.health_countdown > 0: - app.state.health_countdown -= 1 - response.status_code = 404 - status = "unhealthy" - return {"status": status, "user": None} diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..fa86fa0 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "maelstro" +version = "0.1.0" +description = "" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi[standard] (>=0.115.6,<0.116.0)", + "requests (>=2.32.3,<3.0.0)", +] + +[tool.poetry.group.check] +optional = true + +[tool.poetry.group.check.dependencies] +mypy=">=1.14.1,<2.0.0" +types-requests=">=2.32.0.20241016,<3.0.0.0" +pylint=">=3.3.3,<4.0.0" +pyflakes=">=3.2.0,<4.0.0" +black=">=24.10.0,<25.0.0" +pytest = "^8.3.4" + +[tool.poetry.scripts] +health_check = "maelstro.scripts.health_check:check" +serve_dev = "maelstro:dev" +serve_docker_dev = "maelstro:docker_dev" +serve_prod = "maelstro:prod" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pylint.format] +max-line-length = "88" diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_API.py b/backend/tests/test_API.py new file mode 100644 index 0000000..d3322e0 --- /dev/null +++ b/backend/tests/test_API.py @@ -0,0 +1,12 @@ +from fastapi.testclient import TestClient + +from maelstro.main import app + + +client = TestClient(app) + + +def test_read_main(): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {'Hello': 'World'} diff --git a/docker-compose.yml b/docker-compose.yml index 5a2d623..ac18f2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,22 +5,26 @@ include: services: maelstro-back: - build: ./backend + build: + context: ./backend + target: server volumes: - ./backend:/app healthcheck: - test: "/app/health_check.py" + test: "poetry run health_check" interval: 10s retries: 5 start_period: 10s timeout: 10s - ports: - - 8000:8000 - command: - - fastapi - - dev - - --host - - 0.0.0.0 - - main.py + - serve_docker_dev + + check: + profiles: + - check + build: + context: ./backend + target: check + volumes: + - ./backend:/app diff --git a/georchestra/geor-compose.override.yml b/georchestra/geor-compose.override.yml index 49fc190..7042013 100644 --- a/georchestra/geor-compose.override.yml +++ b/georchestra/geor-compose.override.yml @@ -21,4 +21,3 @@ services: retries: 10 env_file: - .envs-common - diff --git a/georchestra/geor-compose.yml b/georchestra/geor-compose.yml index 58f013a..fcbf274 100644 --- a/georchestra/geor-compose.yml +++ b/georchestra/geor-compose.yml @@ -251,5 +251,3 @@ services: discovery.type: single-node ES_JAVA_OPTS: -Xms512m -Xmx512m restart: no - - diff --git a/georchestra/resources/caddy/data/.gitignore b/georchestra/resources/caddy/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/georchestra/resources/caddy/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/georchestra/resources/ssl/.gitignore b/georchestra/resources/ssl/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/georchestra/resources/ssl/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/georchestra/resources/static/.gitignore b/georchestra/resources/static/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/georchestra/resources/static/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore