Merge pull request #99 from sbidoul/modernize

Modernize, require Python 3.12
This commit is contained in:
Stéphane Bidoul 2023-11-19 13:20:20 +01:00 committed by GitHub
commit aca17a06f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 127 additions and 119 deletions

View file

@ -1,11 +1,11 @@
RUNBOAT_REPOS=[{"repo": "^oca/.*", "branch": "^15.0$", "builds": [{"image": "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest"}]}] RUNBOAT_REPOS='[{"repo": "^oca/.*", "branch": "^15.0$", "builds": [{"image": "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest"}]}]'
RUNBOAT_API_ADMIN_USER="admin" RUNBOAT_API_ADMIN_USER="admin"
RUNBOAT_API_ADMIN_PASSWD="admin" RUNBOAT_API_ADMIN_PASSWD="admin"
RUNBOAT_BUILD_NAMESPACE=runboat-builds RUNBOAT_BUILD_NAMESPACE=runboat-builds
RUNBOAT_BUILD_DOMAIN=runboat-builds.odoo-community.org RUNBOAT_BUILD_DOMAIN=runboat-builds.odoo-community.org
RUNBOAT_BUILD_ENV={"PGHOST": "postgres14.runboat-builds-db", "PGPORT": "5432", "PGUSER": "runboat-build"} RUNBOAT_BUILD_ENV='{"PGHOST": "postgres14.runboat-builds-db", "PGPORT": "5432", "PGUSER": "runboat-build"}'
RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "..."} RUNBOAT_BUILD_SECRET_ENV='{"PGPASSWORD": "..."}'
RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"} RUNBOAT_BUILD_TEMPLATE_VARS='{"storageClassName": "my-storage-class"}'
RUNBOAT_BUILD_DEFAULT_KUBEFILES_PATH= RUNBOAT_BUILD_DEFAULT_KUBEFILES_PATH=
RUNBOAT_GITHUB_TOKEN= RUNBOAT_GITHUB_TOKEN=
RUNBOAT_LOG_CONFIG=log-config.yaml RUNBOAT_LOG_CONFIG=log-config.yaml

View file

@ -1,11 +1,11 @@
RUNBOAT_REPOS=[{"repo": "^oca/.*", "branch": "^15.0$", "builds": [{"image": "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest"}]}, {"repo": "^oca/.*", "branch": "^16.0$", "builds": [{"image": "ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest", "kubefiles_path": "/tmp"}]}] RUNBOAT_REPOS='[{"repo": "^oca/.*", "branch": "^15.0$", "builds": [{"image": "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest"}]}, {"repo": "^oca/.*", "branch": "^16.0$", "builds": [{"image": "ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest", "kubefiles_path": "/tmp"}]}]'
RUNBOAT_API_ADMIN_USER="admin" RUNBOAT_API_ADMIN_USER="admin"
RUNBOAT_API_ADMIN_PASSWD="admin" RUNBOAT_API_ADMIN_PASSWD="admin"
RUNBOAT_BUILD_NAMESPACE=runboat-builds RUNBOAT_BUILD_NAMESPACE=runboat-builds
RUNBOAT_BUILD_DOMAIN=runboat.odoo-community.org RUNBOAT_BUILD_DOMAIN=runboat.odoo-community.org
RUNBOAT_BUILD_ENV={} RUNBOAT_BUILD_ENV={}
RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "thepgpassword"} RUNBOAT_BUILD_SECRET_ENV='{"PGPASSWORD": "thepgpassword"}'
RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"} RUNBOAT_BUILD_TEMPLATE_VARS='{"storageClassName": "my-storage-class"}'
RUNBOAT_GITHUB_TOKEN= RUNBOAT_GITHUB_TOKEN=
RUNBOAT_GITHUB_WEBHOOK_SECRET= RUNBOAT_GITHUB_WEBHOOK_SECRET=
RUNBOAT_LOG_CONFIG=log-config.yaml RUNBOAT_LOG_CONFIG=log-config.yaml

View file

@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: [ "3.10", "3.11" ] python-version: ["3.12"]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-python@v2 - uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install project - name: Install project
@ -32,15 +32,15 @@ jobs:
if: ${{ github.repository_owner == 'sbidoul' && github.ref == 'refs/heads/main' }} if: ${{ github.repository_owner == 'sbidoul' && github.ref == 'refs/heads/main' }}
steps: steps:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io - name: Login to ghcr.io
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build image - name: Build image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v5
with: with:
tags: | tags: |
ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:latest

View file

@ -1,18 +1,15 @@
default_language_version: default_language_version:
python: python3 python: python3
repos: repos:
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: check-toml - id: check-toml
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.252 rev: v0.1.6
hooks: hooks:
- id: ruff - id: ruff
- id: ruff-format

View file

@ -1,4 +1,4 @@
FROM python:3.10 FROM python:3.12
LABEL maintainer="Stéphane Bidoul" LABEL maintainer="Stéphane Bidoul"

View file

@ -11,15 +11,18 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"ansi2html", "ansi2html",
"fastapi", "fastapi>=0.93",
"gunicorn", "gunicorn",
"httpx", "httpx",
"jinja2", "jinja2",
"kubernetes", "kubernetes",
"pydantic>=2",
"pydantic-settings",
"rich", "rich",
"sse-starlette", "sse-starlette",
"uvicorn", "uvicorn",
] ]
requires-python = "==3.12.*"
dynamic = ["version", "description"] dynamic = ["version", "description"]
[project.optional-dependencies] [project.optional-dependencies]
@ -38,6 +41,8 @@ mypy = [
[project.urls] [project.urls]
Home = "https://github.com/sbidoul/runboat" Home = "https://github.com/sbidoul/runboat"
# ruff
[tool.ruff] [tool.ruff]
fix = true fix = true
target-version = "py310" target-version = "py310"
@ -60,19 +65,30 @@ max-complexity = 15
[tool.ruff.isort] [tool.ruff.isort]
known-first-party = ["runboat"] known-first-party = ["runboat"]
# pytest
[tool.pytest.ini_options] [tool.pytest.ini_options]
env_override_existing_values = 1 env_override_existing_values = 1
env_files = [".env.test"] env_files = [".env.test"]
asyncio_mode = "strict" asyncio_mode = "strict"
# flake8 config is in .flake8
# mypy
[tool.mypy] [tool.mypy]
strict = true strict = true
show_error_codes = true show_error_codes = true
plugins = ["pydantic.mypy"]
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = ["uvicorn.*", "kubernetes.*", "ansi2html"] module = ["kubernetes.*"]
ignore_missing_imports = true ignore_missing_imports = true
[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
# pip-deepfreeze
[tool.pip-deepfreeze.sync] [tool.pip-deepfreeze.sync]
extras = "test,mypy" extras = "test,mypy"

View file

@ -1,5 +1,4 @@
# frozen requirements generated by pip-deepfreeze # frozen requirements generated by pip-deepfreeze
mypy==0.991 mypy==1.7.0
mypy-extensions==0.4.3 mypy-extensions==1.0.0
tomli==2.0.1 types-urllib3==1.26.25.14
types-urllib3==1.26.25.4

View file

@ -1,15 +1,9 @@
# frozen requirements generated by pip-deepfreeze # frozen requirements generated by pip-deepfreeze
attrs==22.1.0 coverage==7.3.2
coverage==6.5.0 iniconfig==2.0.0
exceptiongroup==1.0.4 pluggy==1.3.0
iniconfig==1.1.1 pytest==7.4.3
packaging==21.3 pytest-asyncio==0.21.1
pluggy==1.0.0 pytest-cov==4.1.0
pyparsing==3.0.9
pytest==7.2.0
pytest-asyncio==0.20.2
pytest-cov==4.0.0
pytest-dotenv==0.5.2 pytest-dotenv==0.5.2
pytest-mock==3.10.0 pytest-mock==3.12.0
python-dotenv==0.21.0
tomli==2.0.1

View file

@ -1,38 +1,43 @@
# frozen requirements generated by pip-deepfreeze # frozen requirements generated by pip-deepfreeze
annotated-types==0.6.0
ansi2html==1.8.0 ansi2html==1.8.0
anyio==3.6.2 anyio==3.7.1
cachetools==5.2.0 cachetools==5.3.2
certifi==2022.9.24 certifi==2023.11.17
charset-normalizer==2.1.1 charset-normalizer==3.3.2
click==8.1.3 click==8.1.7
commonmark==0.9.1 fastapi==0.104.1
fastapi==0.87.0 google-auth==2.23.4
google-auth==2.14.1 gunicorn==21.2.0
gunicorn==20.1.0
h11==0.14.0 h11==0.14.0
httpcore==0.16.1 httpcore==1.0.2
httpx==0.23.1 httpx==0.25.1
idna==3.4 idna==3.4
Jinja2==3.1.2 Jinja2==3.1.2
kubernetes==25.3.0 kubernetes==28.1.0
MarkupSafe==2.1.1 markdown-it-py==3.0.0
MarkupSafe==2.1.3
mdurl==0.1.2
oauthlib==3.2.2 oauthlib==3.2.2
pyasn1==0.4.8 packaging==23.2
pyasn1-modules==0.2.8 pyasn1==0.5.0
pydantic==1.10.2 pyasn1-modules==0.3.0
Pygments==2.13.0 pydantic==2.5.1
pydantic-settings==2.1.0
pydantic_core==2.14.3
Pygments==2.17.1
python-dateutil==2.8.2 python-dateutil==2.8.2
PyYAML==6.0 python-dotenv==1.0.0
requests==2.28.1 PyYAML==6.0.1
requests==2.31.0
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
rfc3986==1.5.0 rich==13.7.0
rich==12.6.0
rsa==4.9 rsa==4.9
six==1.16.0 six==1.16.0
sniffio==1.3.0 sniffio==1.3.0
sse-starlette==1.2.1 sse-starlette==1.6.5
starlette==0.21.0 starlette==0.27.0
typing_extensions==4.4.0 typing_extensions==4.8.0
urllib3==1.26.12 urllib3==1.26.18
uvicorn==0.20.0 uvicorn==0.24.0.post1
websocket-client==1.4.2 websocket-client==1.6.4

View file

@ -5,7 +5,7 @@ from collections.abc import AsyncGenerator
from ansi2html import Ansi2HTMLConverter from ansi2html import Ansi2HTMLConverter
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from sse_starlette.sse import EventSourceResponse from sse_starlette.sse import EventSourceResponse
from starlette.status import HTTP_404_NOT_FOUND from starlette.status import HTTP_404_NOT_FOUND
@ -18,6 +18,8 @@ router = APIRouter()
class Status(BaseModel): class Status(BaseModel):
model_config = ConfigDict(from_attributes=True)
deployed: int deployed: int
max_deployed: int max_deployed: int
failed: int failed: int
@ -29,19 +31,17 @@ class Status(BaseModel):
max_initializing: int max_initializing: int
undeploying: int undeploying: int
class Config:
orm_mode = True
class Repo(BaseModel): class Repo(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str name: str
link: str link: str
class Config:
orm_mode = True
class Build(BaseModel): class Build(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str name: str
commit_info: github.CommitInfo commit_info: github.CommitInfo
deploy_link: str deploy_link: str
@ -54,9 +54,6 @@ class Build(BaseModel):
created: datetime.datetime created: datetime.datetime
last_scaled: datetime.datetime last_scaled: datetime.datetime
class Config:
orm_mode = True
class BuildEvent(BaseModel): class BuildEvent(BaseModel):
event: models.BuildEvent event: models.BuildEvent

View file

@ -1,23 +1,27 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from . import __version__, api, controller, k8s, webhooks, webui from . import __version__, api, controller, k8s, webhooks, webui
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
await k8s.load_kube_config()
await controller.controller.start()
yield
await controller.controller.stop()
app = FastAPI( app = FastAPI(
title="Runboat", description="Runbot on Kubernetes ☸️", version=__version__ title="Runboat",
description="Runbot on Kubernetes ☸️",
version=__version__,
lifespan=lifespan,
) )
app.include_router(api.router, prefix="/api/v1", tags=["api"]) app.include_router(api.router, prefix="/api/v1", tags=["api"])
app.include_router(webhooks.router, tags=["webhooks"]) app.include_router(webhooks.router, tags=["webhooks"])
app.include_router(webui.router, tags=["webui"]) app.include_router(webui.router, tags=["webui"])
webui.mount(app) webui.mount(app)
@app.on_event("startup")
async def startup() -> None:
await k8s.load_kube_config()
await controller.controller.start()
@app.on_event("shutdown")
async def shutdown() -> None:
await controller.controller.stop()

View file

@ -3,7 +3,7 @@ from enum import Enum
from typing import Any from typing import Any
import httpx import httpx
from pydantic import BaseModel, validator from pydantic import BaseModel, field_validator
from .exceptions import NotFoundOnGitHub from .exceptions import NotFoundOnGitHub
from .settings import settings from .settings import settings
@ -32,7 +32,7 @@ class CommitInfo(BaseModel):
pr: int | None pr: int | None
git_commit: str git_commit: str
@validator("repo") @field_validator("repo")
def validate_repo(cls, v: str) -> str: def validate_repo(cls, v: str) -> str:
return v.lower() return v.lower()

View file

@ -183,7 +183,9 @@ def _get_kubefiles_path(kubefiles_path: Path | None) -> Generator[Path, None, No
if kubefiles_path: if kubefiles_path:
yield kubefiles_path yield kubefiles_path
else: else:
with resources.path(__package__, "kubefiles") as default_kubefiles_path: with resources.as_file(
resources.files(__package__).joinpath("kubefiles")
) as default_kubefiles_path:
yield default_kubefiles_path yield default_kubefiles_path

View file

@ -5,7 +5,7 @@ from enum import Enum
from typing import Optional from typing import Optional
from kubernetes.client.models.v1_deployment import V1Deployment from kubernetes.client.models.v1_deployment import V1Deployment
from pydantic import BaseModel from pydantic import BaseModel, ConfigDict
from . import github, k8s from . import github, k8s
from .github import CommitInfo, GitHubStatusState from .github import CommitInfo, GitHubStatusState
@ -38,6 +38,8 @@ class BuildInitStatus(str, Enum):
class Build(BaseModel): class Build(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str name: str
deployment_name: str deployment_name: str
commit_info: CommitInfo commit_info: CommitInfo
@ -47,9 +49,6 @@ class Build(BaseModel):
last_scaled: datetime.datetime last_scaled: datetime.datetime
created: datetime.datetime created: datetime.datetime
class Config:
read_with_orm_mode = True
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.slug} ({self.name})" return f"{self.slug} ({self.name})"
@ -377,11 +376,10 @@ class Build(BaseModel):
class Repo(BaseModel): class Repo(BaseModel):
model_config = ConfigDict(from_attributes=True)
name: str name: str
@property @property
def link(self) -> str: def link(self) -> str:
return f"https://github.com/{self.name}" return f"https://github.com/{self.name}"
class Config:
read_with_orm_mode = True

View file

@ -1,7 +1,9 @@
import re import re
from pathlib import Path from pathlib import Path
from typing import Annotated
from pydantic import BaseModel, BaseSettings, validator from pydantic import BaseModel, BeforeValidator, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from .exceptions import RepoOrBranchNotSupported from .exceptions import RepoOrBranchNotSupported
@ -21,11 +23,7 @@ class BuildSettings(BaseModel):
env: dict[str, str] = {} env: dict[str, str] = {}
secret_env: dict[str, str] = {} secret_env: dict[str, str] = {}
template_vars: dict[str, str] = {} template_vars: dict[str, str] = {}
kubefiles_path: Path | None kubefiles_path: Annotated[Path | None, BeforeValidator(validate_path)] = None
validate_kubefiles_path = validator("kubefiles_path", allow_reuse=True, pre=True)(
validate_path
)
class RepoSettings(BaseModel): class RepoSettings(BaseModel):
@ -33,7 +31,7 @@ class RepoSettings(BaseModel):
branch: str # regex branch: str # regex
builds: list[BuildSettings] builds: list[BuildSettings]
@validator("builds") @field_validator("builds")
def validate_builds(cls, v: list[BuildSettings]) -> list[BuildSettings]: def validate_builds(cls, v: list[BuildSettings]) -> list[BuildSettings]:
if len(v) != 1: if len(v) != 1:
raise ValueError( raise ValueError(
@ -43,6 +41,8 @@ class RepoSettings(BaseModel):
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="RUNBOAT_")
# Configuration for supported repositories and branches. # Configuration for supported repositories and branches.
repos: list[RepoSettings] repos: list[RepoSettings]
# A user and password to protect the most sensitive operations of the API. # A user and password to protect the most sensitive operations of the API.
@ -59,15 +59,17 @@ class Settings(BaseSettings):
# The wildcard domain where the builds will be reacheable. # The wildcard domain where the builds will be reacheable.
build_domain: str build_domain: str
# A dictionary of environment variables to set in the build container and jobs. # A dictionary of environment variables to set in the build container and jobs.
build_env: dict[str, str] = {} build_env: dict[str, str] = {} # noqa: RUF012
# A dictionary of secret environment variables to set in the build container and # A dictionary of secret environment variables to set in the build container and
# jobs. # jobs.
build_secret_env: dict[str, str] = {} build_secret_env: dict[str, str] = {} # noqa: RUF012
# A dictionary of variables to be set in the jinja rendering context for the # A dictionary of variables to be set in the jinja rendering context for the
# kubefiles. # kubefiles.
build_template_vars: dict[str, str] = {} build_template_vars: dict[str, str] = {} # noqa: RUF012
# The path of the default kubefiles to be used. # The path of the default kubefiles to be used.
build_default_kubefiles_path: Path | None build_default_kubefiles_path: Annotated[
Path | None, BeforeValidator(validate_path)
] = None
# The token to use for the GitHub api calls (to query branches and pull requests, # The token to use for the GitHub api calls (to query branches and pull requests,
# and report build statuses). # and report build statuses).
github_token: str | None github_token: str | None
@ -83,10 +85,6 @@ class Settings(BaseSettings):
# Disable posting of statuses to GitHub commits # Disable posting of statuses to GitHub commits
disable_commit_statuses: bool = False disable_commit_statuses: bool = False
validate_build_default_kubefiles_path = validator(
"build_default_kubefiles_path", allow_reuse=True, pre=True
)(validate_path)
def get_build_settings(self, repo: str, target_branch: str) -> list[BuildSettings]: def get_build_settings(self, repo: str, target_branch: str) -> list[BuildSettings]:
for repo_settings in self.repos: for repo_settings in self.repos:
if not re.match(repo_settings.repo, repo, re.IGNORECASE): if not re.match(repo_settings.repo, repo, re.IGNORECASE):
@ -106,8 +104,5 @@ class Settings(BaseSettings):
else: else:
return True return True
class Config:
env_prefix = "RUNBOAT_"
settings = Settings() settings = Settings()

View file

@ -40,7 +40,9 @@ def mount(app: FastAPI) -> None:
directory, which is then mounted under the /webui route. directory, which is then mounted under the /webui route.
""" """
webui_path = Path(__file__).parent / "webui" webui_path = Path(__file__).parent / "webui"
with resources.path("runboat", "webui-templates") as webui_template_path: with resources.as_file(
resources.files(__package__).joinpath("webui-templates")
) as webui_template_path:
for path in webui_template_path.iterdir(): for path in webui_template_path.iterdir():
if path.name.endswith(".jinja"): if path.name.endswith(".jinja"):
template = jinja2.Template(path.read_text()) template = jinja2.Template(path.read_text())

View file

@ -27,7 +27,6 @@ def _make_build(
pr=pr or None, pr=pr or None,
git_commit="0d35a10f161b410f2baa3d416a338d191b6dabc0", git_commit="0d35a10f161b410f2baa3d416a338d191b6dabc0",
), ),
image="ghcr.io/oca/oca-ci:py3.8-odoo15.0",
status=status or BuildStatus.starting, status=status or BuildStatus.starting,
init_status=init_status or BuildInitStatus.todo, init_status=init_status or BuildInitStatus.todo,
desired_replicas=0, desired_replicas=0,