Merge pull request #99 from sbidoul/modernize
Modernize, require Python 3.12
This commit is contained in:
commit
aca17a06f5
17 changed files with 127 additions and 119 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.10
|
||||
FROM python:3.12
|
||||
|
||||
LABEL maintainer="Stéphane Bidoul"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue