Merge pull request #76 from nilshamerlinck/kubefiles_path
Add configurable kubefiles path
This commit is contained in:
commit
31d33ef4fd
9 changed files with 103 additions and 48 deletions
|
|
@ -6,6 +6,7 @@ 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_DEFAULT_KUBEFILES_PATH=
|
||||
RUNBOAT_GITHUB_TOKEN=
|
||||
RUNBOAT_LOG_CONFIG=log-config.yaml
|
||||
RUNBOAT_BASE_URL=https://runboat.odoo-community.org
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
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"}]}, {"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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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_BUILD_TEMPLATE_VARS='{}'
|
||||
ENV RUNBOAT_BUILD_DEFAULT_KUBEFILES_PATH=
|
||||
ENV RUNBOAT_GITHUB_TOKEN=
|
||||
ENV RUNBOAT_GITHUB_WEBHOOK_SECRET=
|
||||
ENV RUNBOAT_BASE_URL=https://runboat.example.com
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -141,6 +141,22 @@ resources:
|
|||
- it removes the deployment finalizers and deletes resources matching the
|
||||
`runboat/build` label after the cleanup job succeeded.
|
||||
|
||||
### Alternative Kubefiles
|
||||
|
||||
By default, Runboat relies on its bundled Kubefiles:
|
||||
[src/runboat/kubefiles](./src/runboat/kubefiles)
|
||||
|
||||
But you can define:
|
||||
|
||||
- a different default path through environment variable
|
||||
`RUNBOAT_BUILD_DEFAULT_KUBEFILES_PATH`;
|
||||
- a different path for a specific repo,
|
||||
by defining the `kubefiles_path` key in `RUNBOAT_REPOS`, e.g.:
|
||||
|
||||
```
|
||||
RUNBOAT_REPOS=[{"repo": "^oca/.*", "branch": "^15.0$", "builds": [{"image": "ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest", "kubefiles_path": "/tmp"}]}]
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
- setup environment variables (start from `.env.sample`, the meaning of the environment
|
||||
|
|
|
|||
|
|
@ -179,11 +179,23 @@ def make_deployment_vars(
|
|||
|
||||
|
||||
@contextmanager
|
||||
def _render_kubefiles(deployment_vars: DeploymentVars) -> Generator[Path, None, None]:
|
||||
with resources.path(
|
||||
__package__, "kubefiles"
|
||||
def _get_kubefiles_path(kubefiles_path: Path | None) -> Generator[Path, None, None]:
|
||||
if kubefiles_path:
|
||||
yield kubefiles_path
|
||||
else:
|
||||
with resources.path(__package__, "kubefiles") as default_kubefiles_path:
|
||||
yield default_kubefiles_path
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _render_kubefiles(
|
||||
kubefiles_path: Path | None, deployment_vars: DeploymentVars
|
||||
) -> Generator[Path, None, None]:
|
||||
with _get_kubefiles_path(
|
||||
kubefiles_path
|
||||
) as kubefiles_path, tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
_logger.debug("kubefiles path: %s", kubefiles_path)
|
||||
# TODO async copytree, or make this whole _render_kubefiles run_in_executor
|
||||
shutil.copytree(kubefiles_path, tmp_path, dirs_exist_ok=True)
|
||||
template = Template((tmp_path / "kustomization.yaml.jinja").read_text())
|
||||
|
|
@ -203,8 +215,8 @@ async def _kubectl(args: list[str]) -> None:
|
|||
raise subprocess.CalledProcessError(return_code, ["kubectl"] + args)
|
||||
|
||||
|
||||
async def deploy(deployment_vars: DeploymentVars) -> None:
|
||||
with _render_kubefiles(deployment_vars) as tmp_path:
|
||||
async def deploy(kubefiles_path: Path | None, deployment_vars: DeploymentVars) -> None:
|
||||
with _render_kubefiles(kubefiles_path, deployment_vars) as tmp_path:
|
||||
# Dry-run first to avoid creating some resources when the creation of the
|
||||
# deployment itself fails. In such cases, we would have resource leak as the
|
||||
# existence of deployment is how the controller knows it has something to
|
||||
|
|
|
|||
|
|
@ -178,22 +178,24 @@ class Build(BaseModel):
|
|||
return await k8s.log(self.name, job_kind=None)
|
||||
|
||||
@classmethod
|
||||
async def deploy(cls, commit_info: CommitInfo) -> None:
|
||||
"""Deploy a build, without starting it."""
|
||||
name = f"b{uuid.uuid4()}"
|
||||
slug = cls.make_slug(commit_info)
|
||||
_logger.info(f"Deploying {slug} ({name}).")
|
||||
async def _deploy(
|
||||
cls, commit_info: CommitInfo, name: str, slug: str, job_kind: k8s.DeploymentMode
|
||||
) -> None:
|
||||
"""Internal method to prepare for and handle a k8s.deploy()"""
|
||||
build_settings = settings.get_build_settings(
|
||||
commit_info.repo, commit_info.target_branch
|
||||
)[0]
|
||||
kubefiles_path = (
|
||||
build_settings.kubefiles_path or settings.build_default_kubefiles_path
|
||||
)
|
||||
deployment_vars = k8s.make_deployment_vars(
|
||||
k8s.DeploymentMode.deployment,
|
||||
job_kind,
|
||||
name,
|
||||
slug,
|
||||
commit_info,
|
||||
build_settings[0],
|
||||
build_settings,
|
||||
)
|
||||
await k8s.deploy(deployment_vars)
|
||||
await k8s.deploy(kubefiles_path, deployment_vars)
|
||||
await github.notify_status(
|
||||
commit_info.repo,
|
||||
commit_info.git_commit,
|
||||
|
|
@ -201,6 +203,16 @@ class Build(BaseModel):
|
|||
target_url=None,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def deploy(cls, commit_info: CommitInfo) -> None:
|
||||
"""Deploy a build, without starting it."""
|
||||
name = f"b{uuid.uuid4()}"
|
||||
slug = cls.make_slug(commit_info)
|
||||
_logger.info(f"Deploying {slug} ({name}).")
|
||||
await cls._deploy(
|
||||
commit_info, name, slug, job_kind=k8s.DeploymentMode.deployment
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start build if init succeeded, or reinitialize if failed."""
|
||||
if self.status not in (BuildStatus.stopped, BuildStatus.stopping):
|
||||
|
|
@ -229,41 +241,27 @@ class Build(BaseModel):
|
|||
_logger.info(f"Redeploying {self}.")
|
||||
await k8s.kill_job(self.name, job_kind=k8s.DeploymentMode.cleanup)
|
||||
await k8s.kill_job(self.name, job_kind=k8s.DeploymentMode.initialize)
|
||||
deployment_vars = k8s.make_deployment_vars(
|
||||
k8s.DeploymentMode.deployment,
|
||||
await self._deploy(
|
||||
self.commit_info,
|
||||
self.name,
|
||||
self.slug,
|
||||
self.commit_info,
|
||||
settings.get_build_settings(
|
||||
self.commit_info.repo, self.commit_info.target_branch
|
||||
)[0],
|
||||
)
|
||||
await k8s.deploy(deployment_vars)
|
||||
await github.notify_status(
|
||||
self.commit_info.repo,
|
||||
self.commit_info.git_commit,
|
||||
GitHubStatusState.pending,
|
||||
target_url=None,
|
||||
job_kind=k8s.DeploymentMode.deployment,
|
||||
)
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Launch the initialization job."""
|
||||
# Start initizalization job. on_initialize_{started,succeeded,failed} callbacks
|
||||
# Start initialization job. on_initialize_{started,succeeded,failed} callbacks
|
||||
# will follow from job events.
|
||||
_logger.info(f"Deploying initialize job for {self}.")
|
||||
deployment_vars = k8s.make_deployment_vars(
|
||||
k8s.DeploymentMode.initialize,
|
||||
await self._deploy(
|
||||
self.commit_info,
|
||||
self.name,
|
||||
self.slug,
|
||||
self.commit_info,
|
||||
settings.get_build_settings(
|
||||
self.commit_info.repo, self.commit_info.target_branch
|
||||
)[0],
|
||||
job_kind=k8s.DeploymentMode.initialize,
|
||||
)
|
||||
await k8s.deploy(deployment_vars)
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
"""Launch the clenaup job."""
|
||||
"""Launch the cleanup job."""
|
||||
# Kill the initialization job to reduce conflict with the cleanup job, such as
|
||||
# the database being created by the initialization after the cleanup job has
|
||||
# completed.
|
||||
|
|
@ -273,16 +271,9 @@ class Build(BaseModel):
|
|||
# Start cleanup job. on_cleanup_{started,succeeded,failed} callbacks will follow
|
||||
# from job events.
|
||||
_logger.info(f"Deploying cleanup job for {self}.")
|
||||
deployment_vars = k8s.make_deployment_vars(
|
||||
k8s.DeploymentMode.cleanup,
|
||||
self.name,
|
||||
self.slug,
|
||||
self.commit_info,
|
||||
settings.get_build_settings(
|
||||
self.commit_info.repo, self.commit_info.target_branch
|
||||
)[0],
|
||||
await self._deploy(
|
||||
self.commit_info, self.name, self.slug, job_kind=k8s.DeploymentMode.cleanup
|
||||
)
|
||||
await k8s.deploy(deployment_vars)
|
||||
|
||||
async def on_initialize_started(self) -> None:
|
||||
if self.init_status == BuildInitStatus.started:
|
||||
|
|
|
|||
|
|
@ -1,16 +1,31 @@
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, BaseSettings, validator
|
||||
|
||||
from .exceptions import RepoOrBranchNotSupported
|
||||
|
||||
|
||||
def validate_path(v: str | None) -> Path | None:
|
||||
if not v:
|
||||
return None
|
||||
p = Path(v)
|
||||
if not p.is_dir():
|
||||
raise ValueError(f"Invalid path: {p}")
|
||||
return p
|
||||
|
||||
|
||||
class BuildSettings(BaseModel):
|
||||
image: str # container image:tag
|
||||
# These extend the respective global settings.
|
||||
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)(
|
||||
validate_path
|
||||
)
|
||||
|
||||
|
||||
class RepoSettings(BaseModel):
|
||||
|
|
@ -51,6 +66,8 @@ class Settings(BaseSettings):
|
|||
# A dictionary of variables to be set in the jinja rendering context for the
|
||||
# kubefiles.
|
||||
build_template_vars: dict[str, str] = {}
|
||||
# The path of the default kubefiles to be used.
|
||||
build_default_kubefiles_path: 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
|
||||
|
|
@ -66,6 +83,10 @@ 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
|
||||
)(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):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
from runboat.github import CommitInfo
|
||||
from runboat.k8s import DeploymentMode, _render_kubefiles, make_deployment_vars
|
||||
from runboat.settings import BuildSettings
|
||||
from runboat.settings import BuildSettings, settings
|
||||
|
||||
EXPECTED = """\
|
||||
resources:
|
||||
|
|
@ -79,6 +79,8 @@ patches:
|
|||
|
||||
|
||||
def test_render_kubefiles() -> None:
|
||||
build_settings = BuildSettings(image="ghcr.io/oca/oca-ci:py3.8-odoo15.0")
|
||||
kubefiles_path = settings.build_default_kubefiles_path
|
||||
deployment_vars = make_deployment_vars(
|
||||
mode=DeploymentMode.deployment,
|
||||
build_name="build-name",
|
||||
|
|
@ -89,9 +91,9 @@ def test_render_kubefiles() -> None:
|
|||
pr=None,
|
||||
git_commit="abcdef123456789",
|
||||
),
|
||||
build_settings=BuildSettings(image="ghcr.io/oca/oca-ci:py3.8-odoo15.0"),
|
||||
build_settings=build_settings,
|
||||
)
|
||||
with _render_kubefiles(deployment_vars) as tmp_path:
|
||||
with _render_kubefiles(kubefiles_path, deployment_vars) as tmp_path:
|
||||
assert (tmp_path / "kustomization.yaml").is_file()
|
||||
assert (tmp_path / "deployment.yaml").is_file()
|
||||
kustomization = (tmp_path / "kustomization.yaml").read_text()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from runboat.exceptions import RepoOrBranchNotSupported
|
||||
|
|
@ -12,3 +14,12 @@ def test_get_build_settings() -> None:
|
|||
settings.get_build_settings("acsone/mis-builder", "15.0")
|
||||
with pytest.raises(RepoOrBranchNotSupported):
|
||||
assert not settings.get_build_settings("OCA/mis-builder", "15.0-stuff")
|
||||
assert settings.get_build_settings("OCA/mis-builder", "15.0") == [
|
||||
BuildSettings(image="ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest")
|
||||
]
|
||||
assert settings.get_build_settings("OCA/mis-builder", "16.0") == [
|
||||
BuildSettings(
|
||||
image="ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest",
|
||||
kubefiles_path=Path("/tmp"),
|
||||
)
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue