From 488d620f40573243cd84138aa40dd8d269117a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 17 Oct 2021 17:24:56 +0200 Subject: [PATCH] WIP - commit/rollback transactions - identify supported branch prefixes - query builds api - better exceptions --- src/runboat/api.py | 32 +++++++++++++++++++------------- src/runboat/build_images.py | 30 +++++++++++++++++++++++++++--- src/runboat/db.py | 11 +++++++---- src/runboat/exceptions.py | 4 ++++ src/runboat/github.py | 2 +- src/runboat/models.py | 20 +++++++++++++++----- 6 files changed, 73 insertions(+), 26 deletions(-) diff --git a/src/runboat/api.py b/src/runboat/api.py index 35e1b04..3d3d6f4 100644 --- a/src/runboat/api.py +++ b/src/runboat/api.py @@ -1,11 +1,11 @@ import datetime -from enum import Enum from typing import List -from fastapi import Depends +from fastapi import Depends, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field from sqlalchemy.orm import Session +from starlette.status import HTTP_404_NOT_FOUND from . import github, models from .app import app @@ -45,14 +45,10 @@ class Branch(BaseModel): response_model=List[Branch], ) def branches(repo_id: str, db: Session = Depends(get_db)): - return db.query(models.Branch).filter(models.Branch.repo_id == repo_id).all() - - -class BuildStatus(str, Enum): - stopped = "stopped" - running = "running" - deploying = "deploying" - not_deployed = "not_deployed" + repo = db.query(models.Repo).get(repo_id) + if not repo: + raise HTTPException(HTTP_404_NOT_FOUND) + return db.query(models.Branch).filter(models.Branch.repo == repo).all() class Build(BaseModel): @@ -60,7 +56,7 @@ class Build(BaseModel): created: datetime.datetime display_name: str display_url: str = Field(title="Link to open the build") - status: BuildStatus + status: models.BuildStatus class Config: orm_mode = True @@ -70,8 +66,18 @@ class Build(BaseModel): "/repos/{repo_id}/branches/{branch_id}/builds", response_model=List[Build], ) -def builds(repo_id: str, branch_id: str): - ... +def builds(repo_id: str, branch_id: str, db: Session = Depends(get_db)): + repo = db.query(models.Repo).get(repo_id) + if not repo: + raise HTTPException(HTTP_404_NOT_FOUND) + branch = ( + db.query(models.Branch) + .filter(models.Branch.id == branch_id, models.Branch.repo == repo) + .one_or_none() + ) + if not branch: + raise HTTPException(HTTP_404_NOT_FOUND) + return db.query(models.Build).filter(models.Build.branch == branch).all() @app.get( diff --git a/src/runboat/build_images.py b/src/runboat/build_images.py index 5a994dc..a759366 100644 --- a/src/runboat/build_images.py +++ b/src/runboat/build_images.py @@ -1,4 +1,6 @@ -from typing import Optional +import re + +from .exceptions import BranchNotSupported images = { "15.0": "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest", @@ -10,5 +12,27 @@ images = { } -def get_build_image(target_branch: str) -> Optional[str]: - return images.get(target_branch) +TARGET_BRANCH_RE = re.compile(r"^(\d+\.\d+)") + + +def get_target_branch(branch_name: str) -> str: + mo = TARGET_BRANCH_RE.match(branch_name) + if not mo: + raise BranchNotSupported( + f"Malformed branch name {branch_name} " + f"(it should start with an Odoo branch name)." + ) + if mo: + key = mo.group(1) + if key not in images: + raise BranchNotSupported( + f"No build image configured for {key} (from {branch_name})." + ) + return key + + +check_branch_supported = get_target_branch + + +def get_build_image(branch_name: str) -> str: + return images[get_target_branch(branch_name)] diff --git a/src/runboat/db.py b/src/runboat/db.py index f6bddd2..3c0e9f5 100644 --- a/src/runboat/db.py +++ b/src/runboat/db.py @@ -1,8 +1,6 @@ -from typing import Generator - from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.orm import sessionmaker from .settings import settings @@ -15,9 +13,14 @@ def create_tables() -> None: Base.metadata.create_all(engine) -def get_db() -> Generator[Session]: +def get_db(): db = SessionLocal() try: yield db + except Exception: + db.rollback() + raise + else: + db.commit() finally: db.close() diff --git a/src/runboat/exceptions.py b/src/runboat/exceptions.py index f0094e2..1d98b87 100644 --- a/src/runboat/exceptions.py +++ b/src/runboat/exceptions.py @@ -12,3 +12,7 @@ class BranchNotFound(ClientError): class NotFoundOnGithub(ClientError): pass + + +class BranchNotSupported(ClientError): + pass diff --git a/src/runboat/github.py b/src/runboat/github.py index fad4f12..1f70c74 100644 --- a/src/runboat/github.py +++ b/src/runboat/github.py @@ -13,7 +13,7 @@ def _github_get(url: str) -> Any: } response = requests.get(full_url, headers) if response.status_code == 404: - raise NotFoundOnGithub(full_url) + raise NotFoundOnGithub(f"GitHub URL not found: {full_url}.") response.raise_for_status() return response.json() diff --git a/src/runboat/models.py b/src/runboat/models.py index 2a0a897..288c066 100644 --- a/src/runboat/models.py +++ b/src/runboat/models.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import List from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func @@ -5,7 +6,7 @@ from sqlalchemy.ext.compiler import compiles from sqlalchemy.orm import Session, relationship from sqlalchemy.sql import expression -from .build_images import get_build_image +from .build_images import check_branch_supported, get_build_image from .db import Base from .exceptions import RepoNotFound from .github import BranchInfo, PullRequestInfo @@ -55,7 +56,7 @@ class Repo(Base): .one_or_none() ) if repo is None: - raise RepoNotFound() + raise RepoNotFound(f"Repo {org}/{name} not supported by this runboat.") return repo @@ -106,6 +107,7 @@ class Branch(Base): .one_or_none() ) if branch is None: + check_branch_supported(branch_info.name) branch = Branch( repo=repo, target_branch=branch_info.name, @@ -128,6 +130,7 @@ class Branch(Base): .one_or_none() ) if branch is None: + check_branch_supported(pr_info.target_branch) branch = Branch( repo=repo, target_branch=pr_info.target_branch, @@ -138,6 +141,13 @@ class Branch(Base): return branch +class BuildStatus(str, Enum): + stopped = "stopped" + running = "running" + deploying = "deploying" + not_deployed = "not_deployed" + + class Build(Base): __tablename__ = "build" @@ -147,9 +157,9 @@ class Build(Base): branch_id = Column(Integer, ForeignKey("branch.id"), nullable=False, index=True) branch: Branch = relationship(Branch, back_populates="builds") - build_image = Column(String, nullable=False) + build_image: str = Column(String, nullable=False) git_sha: str = Column(String, nullable=False) - status: str = Column(String, nullable=False) + status: BuildStatus = Column(String, nullable=False) # ressource_label = Column(String, nullable=False, unique=True, index=True) # TODO: add unique constraint on branch_id + git_sha @@ -182,7 +192,7 @@ class Build(Base): branch=branch, git_sha=git_sha, build_image=build_image, - status="not_deployed", # TODO use same enum as in API + status=BuildStatus.not_deployed, ) db.add(build) db.flush()