0b5d59cf9e
- manifest_git.py: add ProvisionedKeyConfig dataclass; extend GitEntry with ProvisionedKey field (optional); make IdentityFile default to "" so provisioned_key entries can be constructed without a static path; add _parse_provisioned_key_config; update from_repos_entry to accept provisioned_key as an alternative to identity (mutually exclusive, parser rejects both-or-neither) - deploy_key_provisioner.py (new): DeployKeyProvisioner ABC with create() and delete() abstract methods; get_provisioner() factory with lazy contrib import for gitea - contrib/gitea/deploy_key_provisioner.py (new): GiteaDeployKeyProvisioner generating ed25519 keypairs via ssh-keygen and managing them through the Gitea deploy-key API (POST/DELETE); 404 on delete is success; all other errors raise RuntimeError - git_gate.py: add _provision_dynamic_key() called in GitGate.prepare() for entries with ProvisionedKey — generates key, writes private key and key ID files to stage_dir, patches GitGateUpstream.identity_file; add revoke_git_gate_provisioned_keys() for teardown — raises on failure - docker/launch.py: call revoke_git_gate_provisioned_keys() in teardown() after stack.close() so revocation runs after containers stop and failures propagate (not suppressed) - smolmachines/launch.py: extract _teardown_smolmachines() helper that catches stack.close() errors (warn + re-raise) then calls revocation; same fatal-on-failure contract as docker backend - test_manifest_git.py: 9 new cases for provisioned_key parsing - test_deploy_key_provisioner.py (new): factory smoke tests - test_contrib_gitea_deploy_key.py (new): create/delete/error/split tests Closes #169
53 lines
1.9 KiB
Python
53 lines
1.9 KiB
Python
"""Deploy-key provisioner interface and factory (PRD 0048).
|
|
|
|
The core defines the abstract contract; concrete implementations live
|
|
in `bot_bottle/contrib/<provider>/deploy_key_provisioner.py`. The
|
|
factory `get_provisioner` imports contrib modules lazily so that a
|
|
missing optional dependency in one provider doesn't break unrelated
|
|
features."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
|
|
|
|
class DeployKeyProvisioner(ABC):
|
|
"""Manages a single deploy-key lifecycle on a remote forge."""
|
|
|
|
@abstractmethod
|
|
def create(self, owner_repo: str, title: str) -> tuple[str, bytes]:
|
|
"""Generate a keypair and register the public half as a
|
|
deploy key on the forge.
|
|
|
|
`owner_repo` is the `<owner>/<repo>` path (no `.git` suffix).
|
|
`title` is the human-readable label shown in the forge UI.
|
|
|
|
Returns `(key_id, private_key_bytes)` where `key_id` is opaque
|
|
to the caller and is only ever passed back to `delete`."""
|
|
|
|
@abstractmethod
|
|
def delete(self, owner_repo: str, key_id: str) -> None:
|
|
"""Delete the registered deploy key.
|
|
|
|
Must not raise if the key is already absent (HTTP 404 is
|
|
success). Must raise for all other failures so teardown halts."""
|
|
|
|
|
|
def get_provisioner(
|
|
provider: str, token: str, api_url: str
|
|
) -> DeployKeyProvisioner:
|
|
"""Instantiate the contrib provisioner for `provider`.
|
|
|
|
Raises `ManifestError` for unknown providers so the error surfaces
|
|
at parse time rather than at runtime."""
|
|
if provider == "gitea":
|
|
from bot_bottle.contrib.gitea.deploy_key_provisioner import (
|
|
GiteaDeployKeyProvisioner,
|
|
)
|
|
return GiteaDeployKeyProvisioner(token=token, api_url=api_url)
|
|
from .manifest_util import ManifestError
|
|
raise ManifestError(
|
|
f"unknown provisioned_key provider: {provider!r}; "
|
|
f"available: gitea"
|
|
)
|