Some checks failed
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-env-virtual (push) Has been cancelled
CI / test-env-nix (push) Has been cancelled
CI / test-e2e (push) Has been cancelled
CI / test-virgin-user (push) Has been cancelled
CI / test-virgin-root (push) Has been cancelled
CI / codesniffer-shellcheck (push) Has been cancelled
CI / codesniffer-ruff (push) Has been cancelled
Setup configures local Git state, check validates remote reachability in a read-only way, and provision explicitly creates missing remote repositories. Destructive behavior is never implicit. * **Introduce a remote provisioning layer** pkgmgr can now ensure that repositories exist on remote providers. If a repository is missing, it can be created automatically on supported platforms when explicitly requested. * **Add a provider registry for extensibility** Providers are resolved based on the remote host, with optional hints to force a specific backend. This makes it straightforward to add further providers later without changing the core logic. * **Use a lightweight, dependency-free HTTP client** All API communication is handled via a small stdlib-based client. HTTP errors are mapped to meaningful domain errors, improving diagnostics and error handling consistency. * **Centralize credential resolution** API tokens are resolved in a strict order: environment variables first, then the system keyring, and finally an interactive prompt if allowed. This works well for both CI and interactive use. * **Keep keyring integration optional** Secure token storage via the OS keyring is provided as an optional dependency. If unavailable, pkgmgr still works using environment variables or one-off interactive tokens. * **Improve CLI parser safety and clarity** Shared argument helpers now guard against duplicate definitions, making composed subcommands more robust and easier to maintain. * **Expand end-to-end test coverage** All mirror-related workflows are exercised through real CLI invocations in preview mode, ensuring full wiring correctness while remaining safe for automated test environments. https://chatgpt.com/share/693df441-a780-800f-bcf7-96e06cc9e421
98 lines
3.0 KiB
Python
98 lines
3.0 KiB
Python
# src/pkgmgr/core/remote_provisioning/ensure.py
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from pkgmgr.core.credentials.resolver import ResolutionOptions, TokenResolver
|
|
|
|
from .http.errors import HttpError
|
|
from .registry import ProviderRegistry
|
|
from .types import (
|
|
AuthError,
|
|
EnsureResult,
|
|
NetworkError,
|
|
PermissionError,
|
|
ProviderHint,
|
|
RepoSpec,
|
|
UnsupportedProviderError,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EnsureOptions:
|
|
"""Options controlling remote provisioning."""
|
|
|
|
preview: bool = False
|
|
interactive: bool = True
|
|
allow_prompt: bool = True
|
|
save_prompt_token_to_keyring: bool = True
|
|
|
|
|
|
def _raise_mapped_http_error(exc: HttpError, host: str) -> None:
|
|
"""Map HttpError into domain-specific error types."""
|
|
if exc.status == 0:
|
|
raise NetworkError(f"Network error while talking to {host}: {exc}") from exc
|
|
if exc.status == 401:
|
|
raise AuthError(f"Authentication failed for {host} (401).") from exc
|
|
if exc.status == 403:
|
|
raise PermissionError(f"Permission denied for {host} (403).") from exc
|
|
|
|
raise NetworkError(
|
|
f"HTTP error from {host}: status={exc.status}, message={exc}, body={exc.body}"
|
|
) from exc
|
|
|
|
|
|
def ensure_remote_repo(
|
|
spec: RepoSpec,
|
|
provider_hint: Optional[ProviderHint] = None,
|
|
options: Optional[EnsureOptions] = None,
|
|
registry: Optional[ProviderRegistry] = None,
|
|
token_resolver: Optional[TokenResolver] = None,
|
|
) -> EnsureResult:
|
|
"""Ensure that the remote repository exists (create if missing).
|
|
|
|
- Uses TokenResolver (ENV -> keyring -> prompt)
|
|
- Selects provider via ProviderRegistry (or provider_hint override)
|
|
- Respects preview mode (no remote changes)
|
|
- Maps HTTP errors to domain-specific errors
|
|
"""
|
|
opts = options or EnsureOptions()
|
|
reg = registry or ProviderRegistry.default()
|
|
resolver = token_resolver or TokenResolver()
|
|
|
|
provider = reg.resolve(spec.host)
|
|
if provider_hint and provider_hint.kind:
|
|
forced = provider_hint.kind.strip().lower()
|
|
provider = next(
|
|
(p for p in reg.providers if getattr(p, "kind", "").lower() == forced),
|
|
None,
|
|
)
|
|
|
|
if provider is None:
|
|
raise UnsupportedProviderError(f"No provider matched host: {spec.host}")
|
|
|
|
token_opts = ResolutionOptions(
|
|
interactive=opts.interactive,
|
|
allow_prompt=opts.allow_prompt,
|
|
save_prompt_token_to_keyring=opts.save_prompt_token_to_keyring,
|
|
)
|
|
token = resolver.get_token(
|
|
provider_kind=getattr(provider, "kind", "unknown"),
|
|
host=spec.host,
|
|
owner=spec.owner,
|
|
options=token_opts,
|
|
)
|
|
|
|
if opts.preview:
|
|
return EnsureResult(
|
|
status="skipped",
|
|
message="Preview mode: no remote changes performed.",
|
|
)
|
|
|
|
try:
|
|
return provider.ensure_repo(token.token, spec)
|
|
except HttpError as exc:
|
|
_raise_mapped_http_error(exc, host=spec.host)
|
|
return EnsureResult(status="failed", message="Unreachable error mapping.")
|