From e4dc71a0151953f0d67f387cc1485e61d81fce9b Mon Sep 17 00:00:00 2001 From: Defelo Date: Fri, 22 Dec 2023 07:09:32 +0100 Subject: [PATCH 1/4] Add Nix package --- .gitignore | 2 + Dockerfile | 5 -- api/__init__.py | 10 ++++ api/app.py | 6 +- api/version.py | 15 ----- flake.lock | 130 +++++++++++++++++++++++++++++++++++++++++- flake.nix | 18 +++++- pyproject.toml | 7 ++- tests/test_app.py | 6 +- tests/test_version.py | 31 ---------- 10 files changed, 170 insertions(+), 60 deletions(-) delete mode 100644 api/version.py delete mode 100644 tests/test_version.py diff --git a/.gitignore b/.gitignore index 74f425b..044b83e 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,5 @@ dmypy.json .idea .direnv + +result diff --git a/Dockerfile b/Dockerfile index 73741e0..58da327 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,10 +14,6 @@ RUN set -ex \ && . .venv/bin/activate \ && poetry install -n --no-root --without dev -COPY api/version.py /build/ -COPY .git /build/.git/ -RUN python version.py - FROM python:3.11-alpine @@ -37,7 +33,6 @@ EXPOSE 8000 COPY --from=builder /build/.venv/lib /usr/local/lib COPY alembic /app/alembic COPY alembic.ini /app/ -COPY --from=builder /build/VERSION /app/ COPY api /app/api/ diff --git a/api/__init__.py b/api/__init__.py index e69de29..c68983b 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -0,0 +1,10 @@ +def _get_version() -> str: + import tomllib + from pathlib import Path + from typing import cast + + with Path(__file__).parent.parent.joinpath("pyproject.toml").open("rb") as file: + return cast(str, tomllib.load(file)["tool"]["poetry"]["version"]) + + +__version__ = _get_version() diff --git a/api/app.py b/api/app.py index 7c6dd3c..59852c0 100644 --- a/api/app.py +++ b/api/app.py @@ -11,13 +11,13 @@ from fastapi.responses import Response from starlette.exceptions import HTTPException as StarletteHTTPException +from . import __version__ from .database import db, db_context from .endpoints import ROUTER, TAGS from .logger import get_logger, setup_sentry from .settings import settings from .utils.debug import check_responses from .utils.docs import add_endpoint_links_to_openapi_docs -from .version import get_version T = TypeVar("T") @@ -27,7 +27,7 @@ app = FastAPI( title="Bootstrap Academy Backend: Jobs Microservice", description=__doc__, - version=get_version().description, + version=__version__, root_path=settings.root_path, root_path_in_servers=False, servers=[{"url": settings.root_path}] if settings.root_path else None, @@ -44,7 +44,7 @@ def setup_app() -> None: if settings.sentry_dsn: logger.debug("initializing sentry") - setup_sentry(app, settings.sentry_dsn, "jobs-ms", get_version().description) + setup_sentry(app, settings.sentry_dsn, "jobs-ms", __version__) if settings.debug: app.add_middleware( diff --git a/api/version.py b/api/version.py deleted file mode 100644 index 8b1a309..0000000 --- a/api/version.py +++ /dev/null @@ -1,15 +0,0 @@ -import subprocess # noqa: S404 -from collections import namedtuple - - -Version = namedtuple("Version", ["commit", "branch", "description"]) - -cmd = "(git rev-parse HEAD && (git symbolic-ref --short HEAD || echo) && git describe --tags --always) 2> /dev/null" - - -def get_version() -> Version: - return Version(*subprocess.getoutput(cmd + " || cat VERSION").splitlines()) - - -if __name__ == "__main__": - print(subprocess.getoutput(cmd + " | tee VERSION")) diff --git a/flake.lock b/flake.lock index a02675d..c43e386 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,44 @@ { "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-github-actions": { + "inputs": { + "nixpkgs": [ + "poetry2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1698974481, + "narHash": "sha256-yPncV9Ohdz1zPZxYHQf47S8S0VrnhV7nNhCawY46hDA=", + "owner": "nix-community", + "repo": "nix-github-actions", + "rev": "4bb5e752616262457bc7ca5882192a564c0472d2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nix-github-actions", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1699725108, @@ -16,9 +55,98 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1699838902, + "narHash": "sha256-gtaGIer0Fg7QUvsUFsKIjrns32lhnd+o6FvIyokzcKQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6c986b681d6b35affcc4eda42db63595ab01860e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "master", + "repo": "nixpkgs", + "type": "github" + } + }, + "poetry2nix": { + "inputs": { + "flake-utils": "flake-utils", + "nix-github-actions": "nix-github-actions", + "nixpkgs": "nixpkgs_2", + "systems": "systems_2", + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "lastModified": 1703216642, + "narHash": "sha256-9ZxL8UGFLBUepcoujK6CtkGgQhxWkHnDl0r9+h+0uik=", + "owner": "nix-community", + "repo": "poetry2nix", + "rev": "1c75a009920f66138877318358f8a07a8bca443e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "poetry2nix", + "type": "github" + } + }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "poetry2nix": "poetry2nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "id": "systems", + "type": "indirect" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "poetry2nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1699786194, + "narHash": "sha256-3h3EH1FXQkIeAuzaWB+nK0XK54uSD46pp+dMD3gAcB4=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "e82f32aa7f06bbbd56d7b12186d555223dc399d1", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 299678d..7ab2d1a 100644 --- a/flake.nix +++ b/flake.nix @@ -1,9 +1,14 @@ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + poetry2nix.url = "github:nix-community/poetry2nix"; }; - outputs = {nixpkgs, ...}: let + outputs = { + nixpkgs, + poetry2nix, + ... + }: let defaultSystems = [ "x86_64-linux" "x86_64-darwin" @@ -17,6 +22,17 @@ }) defaultSystems); in { + packages = eachDefaultSystem (system: let + pkgs = import nixpkgs {inherit system;}; + inherit (poetry2nix.lib.mkPoetry2Nix {inherit pkgs;}) mkPoetryApplication; + in { + default = mkPoetryApplication { + projectDir = ./.; + python = pkgs.python311; + doCheck = false; + }; + }); + devShells = eachDefaultSystem (system: let pkgs = import nixpkgs {inherit system;}; in { diff --git a/pyproject.toml b/pyproject.toml index 6b76aaa..581a326 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,13 @@ [tool.poetry] name = "jobs-ms" -version = "0" +version = "2.0.2" description = "" authors = ["Defelo "] readme = "README.md" homepage = "https://github.com/Bootstrap-Academy/jobs-ms" repository = "https://github.com/Bootstrap-Academy/jobs-ms" packages = [{ include = "api" }] +include = ["pyproject.toml", "templates/*"] [tool.poetry.dependencies] python = "^3.11" @@ -39,6 +40,10 @@ aiosqlite = "^0.17.0" rich = "^12.5.1" ruff = "^0.1.5" +[tool.poetry.scripts] +api = "api.main:main" +alembic = "alembic.config:main" + [tool.poe.tasks] api = { script = "api.main:main", envfile = ".env" } flake8 = "flake8 . --count --statistics --show-source" diff --git a/tests/test_app.py b/tests/test_app.py index f33a1ee..5e31613 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -28,7 +28,8 @@ def get_decorated_function( async def test__setup_app__sentry(mocker: MockerFixture, monkeypatch: MonkeyPatch) -> None: - get_version_mock = mocker.patch("api.app.get_version") + from api import __version__ + setup_sentry_mock = mocker.patch("api.app.setup_sentry") app_mock = mocker.patch("api.app.app") monkeypatch.setattr(settings, "sentry_dsn", sentry_dsn_mock := MagicMock()) @@ -36,8 +37,7 @@ async def test__setup_app__sentry(mocker: MockerFixture, monkeypatch: MonkeyPatc app.setup_app() - get_version_mock.assert_called_once_with() - setup_sentry_mock.assert_called_once_with(app_mock, sentry_dsn_mock, "jobs-ms", get_version_mock().description) + setup_sentry_mock.assert_called_once_with(app_mock, sentry_dsn_mock, "jobs-ms", __version__) async def test__setup_app__debug(mocker: MockerFixture, monkeypatch: MonkeyPatch) -> None: diff --git a/tests/test_version.py b/tests/test_version.py deleted file mode 100644 index 0459ce0..0000000 --- a/tests/test_version.py +++ /dev/null @@ -1,31 +0,0 @@ -from _pytest.capture import CaptureFixture -from pytest_mock import MockerFixture - -from ._utils import run_module -from api import version - - -async def test__get_version(mocker: MockerFixture) -> None: - getoutput_patch = mocker.patch("subprocess.getoutput") - getoutput_patch.return_value = "2d70111\ndevelop\nv1.0.0" - - result = version.get_version() - - getoutput_patch.assert_called_once_with( - "(git rev-parse HEAD && (git symbolic-ref --short HEAD || echo) && git describe --tags --always) " - "2> /dev/null || cat VERSION" - ) - assert result == version.Version(commit="2d70111", branch="develop", description="v1.0.0") - - -async def test__main(capsys: CaptureFixture[str], mocker: MockerFixture) -> None: - getoutput_patch = mocker.patch("subprocess.getoutput") - getoutput_patch.return_value = "2d70111\ndevelop\nv1.0.0" - - run_module(version) - - getoutput_patch.assert_called_once_with( - "(git rev-parse HEAD && (git symbolic-ref --short HEAD || echo) && git describe --tags --always) " - "2> /dev/null | tee VERSION" - ) - assert capsys.readouterr().out.strip() == getoutput_patch() From f06547d7bf3f95508c0c034a7d87ec3ef921e395 Mon Sep 17 00:00:00 2001 From: Defelo Date: Fri, 22 Dec 2023 07:30:16 +0100 Subject: [PATCH 2/4] Add NixOS module --- flake.nix | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/flake.nix b/flake.nix index 7ab2d1a..aec6646 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,7 @@ }; outputs = { + self, nixpkgs, poetry2nix, ... @@ -33,6 +34,55 @@ }; }); + nixosModules.default = { + config, + lib, + pkgs, + ... + }: let + settingsFormat = pkgs.formats.keyValue {}; + in { + options.academy.backend.jobs = with lib; { + enable = mkEnableOption "Bootstrap Academy Jobs Microservice"; + environmentFiles = mkOption { + type = types.listOf types.path; + }; + settings = mkOption { + inherit (settingsFormat) type; + }; + }; + + config = let + cfg = config.academy.backend.jobs; + in + lib.mkIf cfg.enable { + systemd.services = { + academy-jobs = { + wantedBy = ["multi-user.target"]; + serviceConfig = { + User = "academy-jobs"; + Group = "academy-jobs"; + DynamicUser = true; + EnvironmentFile = cfg.environmentFiles ++ [(settingsFormat.generate "config" cfg.settings)]; + }; + preStart = '' + cd ${lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./alembic + ./alembic.ini + ]; + }} + ${self.packages.${pkgs.system}.default}/bin/alembic upgrade head + ''; + script = '' + ${self.packages.${pkgs.system}.default}/bin/api + ''; + }; + }; + }; + }; + devShells = eachDefaultSystem (system: let pkgs = import nixpkgs {inherit system;}; in { From 5896a5f801a9d52392b7dca7a3bd9e81e64d5ba0 Mon Sep 17 00:00:00 2001 From: Defelo Date: Fri, 22 Dec 2023 07:30:38 +0100 Subject: [PATCH 3/4] Remove docker image --- .github/workflows/docker.yml | 54 ------------------------------------ Dockerfile | 42 ---------------------------- 2 files changed, 96 deletions(-) delete mode 100644 .github/workflows/docker.yml delete mode 100644 Dockerfile diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 16d9b89..0000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: docker - -on: - push: - branches: [develop] - tags: ['v*'] - pull_request: - -permissions: - contents: read - packages: write - -jobs: - docker: - name: docker - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ghcr.io/bootstrap-academy/jobs-ms - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to GHCR - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - provenance: false diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 58da327..0000000 --- a/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -FROM python:3.11-alpine AS builder - -RUN apk add --no-cache build-base gcc musl-dev libffi-dev postgresql14-dev git - -WORKDIR /build - -RUN pip install poetry - -COPY pyproject.toml /build/ -COPY poetry.lock /build/ - -RUN set -ex \ - && virtualenv .venv \ - && . .venv/bin/activate \ - && poetry install -n --no-root --without dev - - -FROM python:3.11-alpine - -LABEL org.opencontainers.image.source="https://github.com/Bootstrap-Academy/jobs-ms" - -WORKDIR /app - -RUN set -x \ - && apk add --no-cache curl libpq \ - && addgroup -g 1000 api \ - && adduser -G api -u 1000 -s /bin/sh -D -H api - -USER api - -EXPOSE 8000 - -COPY --from=builder /build/.venv/lib /usr/local/lib -COPY alembic /app/alembic -COPY alembic.ini /app/ - -COPY api /app/api/ - -HEALTHCHECK --interval=20s --timeout=5s --retries=1 \ - CMD curl -fI http://localhost:${PORT:-8000}/status - -CMD python -m alembic upgrade head && python -m api From 54994480e344eeb188d43c0ae5d3a1d24e03211f Mon Sep 17 00:00:00 2001 From: Defelo Date: Fri, 22 Dec 2023 07:31:16 +0100 Subject: [PATCH 4/4] Add build workflow --- .github/workflows/build.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..fa685bd --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: build + +on: + push: + branches: [develop] + pull_request: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + name: build (${{ matrix.system }}) + strategy: + matrix: + system: [x86_64-linux, aarch64-linux] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - run: sudo apt-get install -y qemu-user-static + - name: Setup Nix + uses: cachix/install-nix-action@v24 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + extra_nix_config: | + system = ${{ matrix.system }} + - name: Setup Cachix + uses: cachix/cachix-action@v13 + with: + name: bootstrap-academy + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - run: nix build