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_PASSWD="admin"
RUNBOAT_BUILD_NAMESPACE=runboat-builds
RUNBOAT_BUILD_DOMAIN=runboat-builds.odoo-community.org
RUNBOAT_BUILD_ENV={"PGHOST": "postgres14.runboat-builds-db", "PGPORT": "5432", "PGUSER": "runboat-build"}
RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "..."}
RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"}
RUNBOAT_BUILD_ENV='{"PGHOST": "postgres14.runboat-builds-db", "PGPORT": "5432", "PGUSER": "runboat-build"}'
RUNBOAT_BUILD_SECRET_ENV='{"PGPASSWORD": "..."}'
RUNBOAT_BUILD_TEMPLATE_VARS='{"storageClassName": "my-storage-class"}'
RUNBOAT_BUILD_DEFAULT_KUBEFILES_PATH=
RUNBOAT_GITHUB_TOKEN=
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_PASSWD="admin"
RUNBOAT_BUILD_NAMESPACE=runboat-builds
RUNBOAT_BUILD_DOMAIN=runboat.odoo-community.org
RUNBOAT_BUILD_ENV={}
RUNBOAT_BUILD_SECRET_ENV={"PGPASSWORD": "thepgpassword"}
RUNBOAT_BUILD_TEMPLATE_VARS={"storageClassName": "my-storage-class"}
RUNBOAT_BUILD_SECRET_ENV='{"PGPASSWORD": "thepgpassword"}'
RUNBOAT_BUILD_TEMPLATE_VARS='{"storageClassName": "my-storage-class"}'
RUNBOAT_GITHUB_TOKEN=
RUNBOAT_GITHUB_WEBHOOK_SECRET=
RUNBOAT_LOG_CONFIG=log-config.yaml

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,23 +1,27 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
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(
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(webhooks.router, tags=["webhooks"])
app.include_router(webui.router, tags=["webui"])
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
import httpx
from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator
from .exceptions import NotFoundOnGitHub
from .settings import settings
@ -32,7 +32,7 @@ class CommitInfo(BaseModel):
pr: int | None
git_commit: str
@validator("repo")
@field_validator("repo")
def validate_repo(cls, v: str) -> str:
return v.lower()

View file

@ -183,7 +183,9 @@ def _get_kubefiles_path(kubefiles_path: Path | None) -> Generator[Path, None, No
if kubefiles_path:
yield kubefiles_path
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

View file

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

View file

@ -1,7 +1,9 @@
import re
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
@ -21,11 +23,7 @@ class BuildSettings(BaseModel):
env: dict[str, str] = {}
secret_env: dict[str, str] = {}
template_vars: dict[str, str] = {}
kubefiles_path: Path | None
validate_kubefiles_path = validator("kubefiles_path", allow_reuse=True, pre=True)(
validate_path
)
kubefiles_path: Annotated[Path | None, BeforeValidator(validate_path)] = None
class RepoSettings(BaseModel):
@ -33,7 +31,7 @@ class RepoSettings(BaseModel):
branch: str # regex
builds: list[BuildSettings]
@validator("builds")
@field_validator("builds")
def validate_builds(cls, v: list[BuildSettings]) -> list[BuildSettings]:
if len(v) != 1:
raise ValueError(
@ -43,6 +41,8 @@ class RepoSettings(BaseModel):
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="RUNBOAT_")
# Configuration for supported repositories and branches.
repos: list[RepoSettings]
# 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.
build_domain: str
# 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
# 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
# kubefiles.
build_template_vars: dict[str, str] = {}
build_template_vars: dict[str, str] = {} # noqa: RUF012
# 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,
# and report build statuses).
github_token: str | None
@ -83,10 +85,6 @@ class Settings(BaseSettings):
# Disable posting of statuses to GitHub commits
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]:
for repo_settings in self.repos:
if not re.match(repo_settings.repo, repo, re.IGNORECASE):
@ -106,8 +104,5 @@ class Settings(BaseSettings):
else:
return True
class Config:
env_prefix = "RUNBOAT_"
settings = Settings()

View file

@ -40,7 +40,9 @@ def mount(app: FastAPI) -> None:
directory, which is then mounted under the /webui route.
"""
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():
if path.name.endswith(".jinja"):
template = jinja2.Template(path.read_text())

View file

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