bd31a3f2f3
- 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.
60 lines
1.8 KiB
Python
60 lines
1.8 KiB
Python
"""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"]
|