diff --git a/.gitignore b/.gitignore index c040887..3b3679b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ __pycache__/ .pytest_cache/ docs/dist/ requirements.txt -app/scss/_vars.scss +app/_version.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bd510b5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.10-slim as python-base +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + POETRY_HOME="/opt/poetry" \ + POETRY_VIRTUALENVS_IN_PROJECT=true \ + POETRY_NO_INTERACTION=1 \ + PYSETUP_PATH="/opt/venv" \ + VENV_PATH="/opt/venv/.venv" +ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" + +FROM python-base as builder-base +RUN apt-get update +RUN apt-get install -y --no-install-recommends curl build-essential gcc +RUN curl -sSL https://install.python-poetry.org | python3 - +WORKDIR $PYSETUP_PATH +COPY poetry.lock pyproject.toml ./ +RUN poetry install --no-dev + +FROM python-base as production +RUN groupadd --gid 1000 microblogpub \ + && useradd --uid 1000 --gid microblogpub --shell /bin/bash microblogpub +COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH +COPY . /app/ +RUN chown -R 1000:1000 /app +USER microblogpub +WORKDIR /app +EXPOSE 8000 +CMD ["supervisord", "-n", "-c", "misc/docker-supervisord.conf"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e284116 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +SHELL := /bin/bash +PWD=$(shell pwd) + +.PHONY: config +config: + # Run and remove instantly + -docker run --rm -it --volume `pwd`/data:/app/data microblogpub/microblogpub inv configuration-wizard diff --git a/app/actor.py b/app/actor.py index eb60c82..7b6803a 100644 --- a/app/actor.py +++ b/app/actor.py @@ -145,7 +145,7 @@ async def save_actor(db_session: AsyncSession, ap_actor: ap.RawObject) -> "Actor handle=_handle(ap_actor), ) db_session.add(actor) - await db_session.flush() + await db_session.commit() return actor diff --git a/app/config.py b/app/config.py index 87e833d..26b4f90 100644 --- a/app/config.py +++ b/app/config.py @@ -1,6 +1,5 @@ import os import secrets -import subprocess from pathlib import Path import bcrypt @@ -19,11 +18,13 @@ ROOT_DIR = Path().parent.resolve() _CONFIG_FILE = os.getenv("MICROBLOGPUB_CONFIG_FILE", "profile.toml") -VERSION_COMMIT = ( - subprocess.check_output(["git", "rev-parse", "--short=8", "HEAD"]) - .split()[0] - .decode() -) +VERSION_COMMIT = "dev" + +try: + from app._version import VERSION_COMMIT # type: ignore +except ImportError: + pass + VERSION = f"2.0.0+{VERSION_COMMIT}" USER_AGENT = f"microblogpub/{VERSION}" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2cd87ce --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" + +services: + server: + image: microblogpub/microblogpub:latest + container_name: microblogpub + user: 1000:1000 + restart: always + volumes: + - ./data:/app/data + ports: + - "8000:8000" diff --git a/misc/docker-supervisord.conf b/misc/docker-supervisord.conf new file mode 100644 index 0000000..d57eeac --- /dev/null +++ b/misc/docker-supervisord.conf @@ -0,0 +1,28 @@ +[supervisord] +nodaemon=true +logfile=/dev/null +logfile_maxbytes=0 +pidfile=data/supervisord.pid + +[fcgi-program:uvicorn] +socket=tcp://0.0.0.0:8000 +command=uvicorn app.main:app --no-server-header --fd 0 +numprocs=2 +process_name=uvicorn-%(process_num)d +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:incoming_worker] +command=inv process-incoming-activities +numproc=1 +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:outgoing_worker] +command=inv process-outgoing-activities +numproc=1 +redirect_stderr=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 diff --git a/poetry.lock b/poetry.lock index 2ee7bb6..cee7bbf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -161,7 +161,7 @@ testing = ["pytest"] name = "boussole" version = "2.0.0" description = "Commandline interface to build Sass projects using libsass-python" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -248,7 +248,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "colorlog" version = "6.6.0" description = "Add colours to the output of Python's logging module." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -489,7 +489,7 @@ python-versions = "*" name = "invoke" version = "1.7.1" description = "Pythonic task execution" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -533,7 +533,7 @@ i18n = ["Babel (>=2.7)"] name = "libsass" version = "0.21.0" description = "Sass for Python: A straightforward binding of libsass for Python." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -741,7 +741,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" name = "pyaml" version = "21.10.1" description = "PyYAML-based module to produce pretty and readable YAML-serialized data" -category = "dev" +category = "main" optional = false python-versions = "*" @@ -894,7 +894,7 @@ six = ">=1.4.0" name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1022,6 +1022,17 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +[[package]] +name = "supervisor" +version = "4.2.4" +description = "A system for controlling process state under UNIX" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +testing = ["pytest", "pytest-cov"] + [[package]] name = "tabulate" version = "0.8.10" @@ -1165,7 +1176,7 @@ standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "p name = "watchdog" version = "2.1.9" description = "Filesystem events monitoring" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -1202,7 +1213,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "d0c330a9153ced7f4ba61e5354bc7bb3136a3d11f4f700bb76498c20e200c509" +content-hash = "5ee42d968baa21950366d1ca0597fb1e0e45e6e26005f93acbcf8b43cd1fb370" [metadata.files] aiosqlite = [ @@ -2002,6 +2013,7 @@ starlette = [ {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, ] +supervisor = [] tabulate = [] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, diff --git a/pyproject.toml b/pyproject.toml index dd30376..4f2fc3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ cachetools = "^5.2.0" humanize = "^4.2.3" tabulate = "^0.8.10" asgiref = "^3.5.2" +supervisor = "^4.2.4" +invoke = "^1.7.1" +boussole = "^2.0.0" [tool.poetry.dev-dependencies] black = "^22.3.0" diff --git a/tasks.py b/tasks.py index f96106a..aa8cdf1 100644 --- a/tasks.py +++ b/tasks.py @@ -1,7 +1,10 @@ import asyncio import io +import subprocess import tarfile +from contextlib import contextmanager from pathlib import Path +from typing import Generator from typing import Optional import httpx @@ -13,13 +16,13 @@ from invoke import task # type: ignore @task def generate_db_migration(ctx, message): # type: (Context, str) -> None - run(f'poetry run alembic revision --autogenerate -m "{message}"', echo=True) + run(f'alembic revision --autogenerate -m "{message}"', echo=True) @task def migrate_db(ctx): # type: (Context) -> None - run("poetry run alembic upgrade head", echo=True) + run("alembic upgrade head", echo=True) @task @@ -46,15 +49,15 @@ def compile_scss(ctx, watch=False): theme_file.write_text("// override vars for theming here") if watch: - run("poetry run boussole watch", echo=True) + run("boussole watch", echo=True) else: - run("poetry run boussole compile", echo=True) + run("boussole compile", echo=True) @task def uvicorn(ctx): # type: (Context) -> None - run("poetry run uvicorn app.main:app --no-server-header", pty=True, echo=True) + run("uvicorn app.main:app --no-server-header", pty=True, echo=True) @task @@ -96,16 +99,11 @@ def generate_requirements_txt(ctx, where="requirements.txt"): ) -@task(generate_requirements_txt) -def build_configuration_wizard_image(ctx): - # type: (Context) -> None - run("docker build -t testmpw -f configuration_wizard.dockerfile .") - - @task def build_docs(ctx): # type: (Context) -> None - run("PYTHONPATH=. poetry run python scripts/build_docs.py", pty=True, echo=True) + with embed_version(): + run("PYTHONPATH=. python scripts/build_docs.py", pty=True, echo=True) @task @@ -131,7 +129,7 @@ def download_twemoji(ctx): @task(download_twemoji, compile_scss, migrate_db) def configuration_wizard(ctx): # type: (Context) -> None - run("PYTHONPATH=. poetry run python scripts/config_wizard.py", pty=True, echo=True) + run("PYTHONPATH=. python scripts/config_wizard.py", pty=True, echo=True) @task @@ -152,3 +150,26 @@ def stats(ctx): from app.utils.stats import print_stats print_stats() + + +@contextmanager +def embed_version() -> Generator[None, None, None]: + version_file = Path("app/_version.py") + version_file.unlink(missing_ok=True) + version = ( + subprocess.check_output(["git", "rev-parse", "--short=8", "v2"]) + .split()[0] + .decode() + ) + version_file.write_text(f'VERSION_COMMIT = "{version}"') + try: + yield + finally: + version_file.unlink() + + +@task +def build_docker_image(ctx): + # type: (Context) -> None + with embed_version(): + run("docker build -t microblogpub/microblogpub .")