v0.27.3
This commit is contained in:
1 parent
e84df445c6
commit
e87a382a7e
109 files changed
+1092
-2925
No files matched your search
Executable
+124
@@ -0,0 +1,124 @@
|
||||
"""RustProxy administration API helpers for NPM migration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from migrate_npm_core import *
|
||||
|
||||
class RustProxyClient:
|
||||
def __init__(self, base_url: str, *, ca_file: Path | None = None, insecure_tls: bool = False):
|
||||
parsed = urllib.parse.urlsplit(base_url.rstrip("/"))
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.path not in {"", "/"}:
|
||||
raise MigrationError("--rustproxy must be an http(s) origin, e.g. https://rustproxy.example:9090")
|
||||
if parsed.query or parsed.fragment or parsed.username or parsed.password:
|
||||
raise MigrationError("--rustproxy must not contain credentials, query or fragment")
|
||||
self.base_url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, "", "", ""))
|
||||
jar = http.cookiejar.CookieJar()
|
||||
handlers: list[Any] = [urllib.request.HTTPCookieProcessor(jar)]
|
||||
if parsed.scheme == "https":
|
||||
if insecure_tls:
|
||||
context = ssl._create_unverified_context() # Explicit operator opt-in for local/self-signed admin TLS.
|
||||
else:
|
||||
context = ssl.create_default_context(cafile=str(ca_file) if ca_file else None)
|
||||
handlers.append(urllib.request.HTTPSHandler(context=context))
|
||||
self.opener = urllib.request.build_opener(*handlers)
|
||||
self.csrf = ""
|
||||
|
||||
def request(self, method: str, path: str, body: dict[str, Any] | None = None, *, authenticated_write: bool = False) -> Any:
|
||||
data = None if body is None else json.dumps(body, separators=(",", ":")).encode()
|
||||
headers = {"Accept": "application/json", "X-RustProxy-Request": "1"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
if method != "GET":
|
||||
headers["Origin"] = self.base_url
|
||||
if authenticated_write:
|
||||
headers["X-CSRF-Token"] = self.csrf
|
||||
request = urllib.request.Request(self.base_url + path, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with self.opener.open(request, timeout=10) as response:
|
||||
payload = response.read(1024 * 1024)
|
||||
except urllib.error.HTTPError as error:
|
||||
payload = error.read(65536)
|
||||
try:
|
||||
detail = json.loads(payload).get("error", payload.decode("utf-8", "replace"))
|
||||
except Exception:
|
||||
detail = payload.decode("utf-8", "replace")
|
||||
raise MigrationError(f"RustProxy API {method} {path} failed with HTTP {error.code}: {detail}") from error
|
||||
except (OSError, urllib.error.URLError) as error:
|
||||
raise MigrationError(f"RustProxy API connection failed: {error}") from error
|
||||
try:
|
||||
return json.loads(payload)
|
||||
except json.JSONDecodeError as error:
|
||||
raise MigrationError(f"RustProxy API returned invalid JSON for {path}") from error
|
||||
|
||||
def login(self, username: str, password: str) -> None:
|
||||
info = self.request("POST", "/api/auth/login", {"username": username, "password": password})
|
||||
token = info.get("csrf_token") if isinstance(info, dict) else None
|
||||
if not isinstance(token, str) or not token:
|
||||
session = self.request("GET", "/api/auth/session")
|
||||
token = session.get("csrf_token") if isinstance(session, dict) else None
|
||||
if not isinstance(token, str) or not token:
|
||||
raise MigrationError("RustProxy login succeeded but no CSRF token was returned")
|
||||
self.csrf = token
|
||||
|
||||
def config(self) -> dict[str, Any]:
|
||||
value = self.request("GET", "/api/config")
|
||||
if not isinstance(value, dict) or not isinstance(value.get("revision"), int) or not isinstance(value.get("config"), dict):
|
||||
raise MigrationError("unexpected RustProxy /api/config response")
|
||||
return value
|
||||
|
||||
def apply(self, revision: int, config: dict[str, Any]) -> dict[str, Any]:
|
||||
value = self.request("PUT", "/api/config", {"expected_revision": revision, "config": config}, authenticated_write=True)
|
||||
if not isinstance(value, dict):
|
||||
raise MigrationError("unexpected RustProxy PUT /api/config response")
|
||||
return value
|
||||
|
||||
|
||||
|
||||
def write_private_json(path: Path, value: dict[str, Any]) -> None:
|
||||
path = path.expanduser()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if path.is_symlink():
|
||||
raise MigrationError(f"refusing to write migration plan through symlink: {path}")
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
fd = os.open(path, flags, 0o600)
|
||||
try:
|
||||
os.fchmod(fd, 0o600)
|
||||
payload = (json.dumps(value, indent=2, ensure_ascii=False) + "\n").encode("utf-8")
|
||||
with os.fdopen(fd, "wb", closefd=False) as stream:
|
||||
stream.write(payload)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
def split_ids(values: list[str] | None) -> list[str] | None:
|
||||
if values is None:
|
||||
return None
|
||||
out: list[str] = []
|
||||
for value in values:
|
||||
for item in value.split(","):
|
||||
item = item.strip()
|
||||
if item and item not in out:
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def print_report(decisions: list[Decision]) -> None:
|
||||
imported = sum(1 for d in decisions if d.action == "import")
|
||||
skipped = sum(1 for d in decisions if d.action == "skip")
|
||||
print(f"NPM proxy hosts: {len(decisions)}; import: {imported}; skip: {skipped}")
|
||||
for decision in decisions:
|
||||
print(f"\n[{decision.action.upper()}] NPM #{decision.npm_id}")
|
||||
if decision.hosts:
|
||||
print(" hosts: " + ", ".join(decision.hosts))
|
||||
if decision.backend:
|
||||
print(" backend: " + decision.backend)
|
||||
if decision.rustproxy_ids:
|
||||
print(" ids: " + ", ".join(decision.rustproxy_ids))
|
||||
if decision.reason:
|
||||
print(" reason: " + decision.reason)
|
||||
for warning in decision.warnings or []:
|
||||
print(" warning: " + warning)
|
||||
|
||||
|
||||
Reference in new issue
Block a user