diff --git a/.env.sample b/.env.sample index 2cff827..dded35b 100644 --- a/.env.sample +++ b/.env.sample @@ -2,8 +2,9 @@ RUNBOAT_SUPPORTED_REPOS=["OCA/mis-builder", "shopinvader/odoo-shopinvader", "OCA RUNBOAT_API_ADMIN_USER="admin" RUNBOAT_API_ADMIN_PASSWD="admin" RUNBOAT_BUILD_NAMESPACE=runboat-builds -RUNBOAT_BUILD_DOMAIN=runboat.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_SECRET_ENV={"PGPASSWORD": "..."} RUNBOAT_GITHUB_TOKEN= RUNBOAT_LOG_CONFIG=log-config.yaml +RUNBOAT_BASE_URL=https://runboat.odoo-community.org diff --git a/Dockerfile b/Dockerfile index fc30936..99950bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,11 +16,12 @@ ENV RUNBOAT_SUPPORTED_REPOS='["OCA/mis-builder", "shopinvader/odoo-shopinvader", ENV RUNBOAT_API_ADMIN_USER="admin" ENV RUNBOAT_API_ADMIN_PASSWD="admin" ENV RUNBOAT_BUILD_NAMESPACE=runboat-builds -ENV RUNBOAT_BUILD_DOMAIN=runboat.example.com +ENV RUNBOAT_BUILD_DOMAIN=runboat-builds.example.com ENV RUNBOAT_BUILD_ENV='{"PGHOST": "postgres14.runboat-builds-db", "PGPORT": "5432", "PGUSER": "runboat-build"}' ENV RUNBOAT_BUILD_SECRET_ENV='{"PGPASSWORD": "..."}' ENV RUNBOAT_GITHUB_TOKEN= ENV RUNBOAT_LOG_CONFIG=/etc/runboat-log-config.yaml +ENV RUNBOAT_BASE_URL=https://runboat.example.com ENV KUBECONFIG=/run/kubeconfig diff --git a/README.md b/README.md index aa68c1e..9523f97 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Runboat has the following main components: accessible; - when the initializaiton job fails, flag the deployment as failed; - when there are too many deployments started, stop the oldest started; - - when there are too many deployments, deleted the oldest created; + - when there are too many deployments, delete the oldest created; - when a deployment is deleted, run a cleanp job to drop the database and delete all kubernetes resources associated with the deployment. @@ -114,6 +114,8 @@ actually deploy. It expects the following to hold true: - `runboat/pr`: the pull request number if this build is for a pull request; - `runboat/git-commit`: the commit sha. +- the home page of a running build is exposed at `http://{build_slug}.{build_domain}`. + During the lifecycle of a build, the controller does the following on the deployed resources: @@ -139,9 +141,6 @@ MVP: - look at other TODO in code to see if anything important remains - basic UI (single page with a combo box to select repo and show builds by branch/pr, with start/stop buttons) -- better target_url in GitHub status: instead of providing the link to the ingress, - provide a link to the build, which redirects to the ingress if the build is started, - or to a build details page with action buttons (start, stop, view log, etc) - secure github webhooks - deployment and more load testing diff --git a/src/runboat/app.py b/src/runboat/app.py index fb52cd3..aaeeb46 100644 --- a/src/runboat/app.py +++ b/src/runboat/app.py @@ -1,12 +1,13 @@ from fastapi import FastAPI -from . import __version__, api, controller, k8s, webhooks +from . import __version__, api, controller, k8s, webhooks, webui app = FastAPI( title="Runboat", description="Runbot on Kubernetes ☸️", version=__version__ ) -app.include_router(api.router, prefix="/api/v1") -app.include_router(webhooks.router) +app.include_router(api.router, prefix="/api/v1", tags=["api"]) +app.include_router(webhooks.router, tags=["webhooks"]) +app.include_router(webui.router, tags=["webui"]) @app.on_event("startup") diff --git a/src/runboat/models.py b/src/runboat/models.py index e61bba8..d742dcd 100644 --- a/src/runboat/models.py +++ b/src/runboat/models.py @@ -132,6 +132,18 @@ class Build(BaseModel): def link(self) -> str: return f"http://{self.slug}.{settings.build_domain}" + @property + def repo_link(self) -> str: + link = f"https://github.com/{self.repo}" + if self.pr: + return f"{link}/pull/{self.pr}" + else: + return f"{link}/tree/{self.target_branch}" + + @property + def live_link(self) -> str: + return f"{settings.base_url}/builds/{self.name}?live" + @classmethod async def deploy( cls, repo: str, target_branch: str, pr: int | None, git_commit: str @@ -178,7 +190,7 @@ class Build(BaseModel): self.repo, self.git_commit, GitHubStatusState.pending, - target_url=None, + target_url=self.live_link, ) elif self.status in (BuildStatus.stopped, BuildStatus.stopping): _logger.info(f"Starting {self} that was last scaled on {self.last_scaled}.") @@ -246,7 +258,7 @@ class Build(BaseModel): self.repo, self.git_commit, GitHubStatusState.pending, - target_url=None, + target_url=self.live_link, ) async def on_initialize_succeeded(self) -> None: @@ -260,7 +272,7 @@ class Build(BaseModel): self.repo, self.git_commit, GitHubStatusState.success, - target_url=self.link, + target_url=self.live_link, ) async def on_initialize_failed(self) -> None: @@ -274,7 +286,7 @@ class Build(BaseModel): self.repo, self.git_commit, GitHubStatusState.failure, - target_url=None, + target_url=self.live_link, ) async def on_cleanup_started(self) -> None: diff --git a/src/runboat/settings.py b/src/runboat/settings.py index fb76f76..538cfcf 100644 --- a/src/runboat/settings.py +++ b/src/runboat/settings.py @@ -38,6 +38,9 @@ class Settings(BaseSettings): github_token: Optional[str] # The file with the python logging configuration to use for the runboat controller. log_config: Optional[str] + # The base url where the runboat UI and API is exposed on internet. + # Used to generate backlinks in GitHub statuses + base_url: str = "http://localhost:8000" class Config: env_prefix = "RUNBOAT_" diff --git a/src/runboat/webui.py b/src/runboat/webui.py new file mode 100644 index 0000000..175f33d --- /dev/null +++ b/src/runboat/webui.py @@ -0,0 +1,24 @@ +from pathlib import Path +from typing import Optional + +from fastapi import APIRouter, HTTPException, Request, status +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates + +from .controller import controller +from .models import BuildStatus + +router = APIRouter() +templates = Jinja2Templates(directory=Path(__file__).parent / "webui") + + +@router.get("/builds/{name}", response_class=HTMLResponse) +async def build(request: Request, name: str, live: Optional[str] = None): + build = controller.db.get(name) + if not build: + raise HTTPException(status.HTTP_404_NOT_FOUND) + if live is not None and build.status == BuildStatus.started: + return RedirectResponse(url=build.link) + return templates.TemplateResponse( + "build.html", {"request": request, "build": build} + ) diff --git a/src/runboat/webui/build.html b/src/runboat/webui/build.html new file mode 100644 index 0000000..8c0b222 --- /dev/null +++ b/src/runboat/webui/build.html @@ -0,0 +1,29 @@ + +
+Repo: {{ build.repo }}
+ {% if build.pr %} +PR: {{ build.pr }} to {{ build.target_branch }}
+ {% else %} +Branch: {{ build.target_branch }}
+ {% endif %} +Commit: {{ build.git_commit }}
+Status: {{ build.status }}
+ {% if build.status == 'started' %} + + + {% else %} + + {% endif %} + +