Phase A1: bootstrap Python ci_orchestrator package
- pyproject.toml (hatchling) with ruff/mypy/pytest dev deps - src/ci_orchestrator/: config, credentials, backends (Protocol + WorkstationVmrunBackend), transport (WinRM via pypsrp, SSH via paramiko) - CLI entry point with PoC 'wait-ready' subcommand (Windows + Linux guests) - tests/python/: 35 unit tests, 83% coverage, all backends/transport mocked - gitea/workflows/lint.yml: new 'python' job (ruff + mypy --strict + pytest --cov-fail-under=70) - config.example.toml + README setup section + .gitignore Python entries Neutral VmBackend Protocol prepared for Phase C ESXi extension. See plans/implementation-plan-A-B.md step A1.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
"""Credential store abstraction.
|
||||
|
||||
Wraps the ``keyring`` library so callers do not depend on it directly.
|
||||
Phase B note: the same interface is used on both Windows (Credential
|
||||
Manager) and Linux (Secret Service / file-based vault); see
|
||||
``plans/implementation-plan-A-B.md`` step B3.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Credential:
|
||||
"""Username + secret pair retrieved from a credential store."""
|
||||
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class CredentialStore(Protocol):
|
||||
"""Read-only credential lookup."""
|
||||
|
||||
def get(self, target: str) -> Credential:
|
||||
"""Return the credential associated with ``target``.
|
||||
|
||||
Raises :class:`KeyError` if the target is unknown.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class KeyringCredentialStore:
|
||||
"""Default :class:`CredentialStore` backed by the ``keyring`` library."""
|
||||
|
||||
def __init__(self, service: str = "ci") -> None:
|
||||
self._service = service
|
||||
|
||||
def get(self, target: str) -> Credential:
|
||||
try:
|
||||
import keyring
|
||||
except ImportError as exc: # pragma: no cover - dep declared
|
||||
raise RuntimeError(
|
||||
"keyring is not installed; run `pip install keyring`."
|
||||
) from exc
|
||||
|
||||
cred = keyring.get_credential(target, None)
|
||||
if cred is None:
|
||||
# Fallback: some backends only support get_password and need the
|
||||
# username to be the same as the target.
|
||||
password = keyring.get_password(self._service, target)
|
||||
if password is None:
|
||||
raise KeyError(f"Credential '{target}' not found in keyring.")
|
||||
return Credential(username=target, password=password)
|
||||
return Credential(username=cred.username, password=cred.password)
|
||||
|
||||
|
||||
__all__ = ["Credential", "CredentialStore", "KeyringCredentialStore"]
|
||||
Reference in New Issue
Block a user