feat: implement update-service v1 migration and enrollment flow
- added /update/enroll endpoint and enrollment logic - migrated update client to v1 api endpoints and bearer auth - implemented remote status reporting in backend and scripts - updated requirements and project status
This commit is contained in:
@ -70,20 +70,56 @@ def get_status(settings: Settings) -> Dict[str, Any]:
|
||||
"last_status": data.get("status", "unknown"),
|
||||
"last_error": data.get("error") or None,
|
||||
"last_timestamp": data.get("timestamp"),
|
||||
"enrolled": bool(settings.update_token),
|
||||
}
|
||||
|
||||
|
||||
def enroll(settings: Settings) -> str:
|
||||
if not settings.update_enroll_token:
|
||||
raise ValueError("No enrollment token provided in settings")
|
||||
|
||||
enroll_url = f"{settings.update_service_url}/v1/enroll"
|
||||
payload = {
|
||||
"project_id": settings.update_project_id,
|
||||
"client_id": os.uname().nodename,
|
||||
"software_id": "safe-kiddo",
|
||||
"enroll_token": settings.update_enroll_token,
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
response = client.post(enroll_url, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
token = data.get("token")
|
||||
if not token:
|
||||
raise ValueError("Enrollment response did not contain a token")
|
||||
|
||||
# Save token
|
||||
token_path = Path(settings.update_token_file)
|
||||
_ensure_parent(token_path)
|
||||
token_path.write_text(token, encoding="utf-8")
|
||||
# Update settings object for immediate use
|
||||
settings.update_token = token
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def _parse_version(value: str) -> List[int]:
|
||||
return [int(part) for part in value.split(".")]
|
||||
|
||||
|
||||
def check_update(settings: Settings) -> Dict[str, Any]:
|
||||
headers = {}
|
||||
if settings.update_token:
|
||||
headers["Authorization"] = f"Bearer {settings.update_token}"
|
||||
if not settings.update_token:
|
||||
raise ValueError("Client is not enrolled (missing update token)")
|
||||
|
||||
headers = {"Authorization": f"Bearer {settings.update_token}"}
|
||||
manifest_url = (
|
||||
f"{settings.update_service_url}/v1/projects/{settings.update_project_id}/manifest"
|
||||
)
|
||||
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
response = client.get(settings.update_url, headers=headers)
|
||||
response = client.get(manifest_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
manifest = response.json()
|
||||
|
||||
@ -109,21 +145,65 @@ def check_update(settings: Settings) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def report_status(
|
||||
settings: Settings,
|
||||
status: str,
|
||||
version: str,
|
||||
error: str | None = None,
|
||||
duration_ms: int | None = None,
|
||||
) -> None:
|
||||
if not settings.update_token:
|
||||
return
|
||||
|
||||
report_url = (
|
||||
f"{settings.update_service_url}/v1/projects/{settings.update_project_id}/status"
|
||||
)
|
||||
payload = {
|
||||
"project_id": settings.update_project_id,
|
||||
"version": version,
|
||||
"status": status,
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"client_id": os.uname().nodename,
|
||||
}
|
||||
if error:
|
||||
payload["error_code"] = error
|
||||
payload["reason"] = error
|
||||
if duration_ms is not None:
|
||||
payload["duration_ms"] = duration_ms
|
||||
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {settings.update_token}"}
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
client.post(report_url, json=payload, headers=headers).raise_for_status()
|
||||
except Exception:
|
||||
# We don't want to crash if status reporting fails
|
||||
pass
|
||||
|
||||
|
||||
def _run_async(script_path: Path, settings: Settings) -> None:
|
||||
env = os.environ.copy()
|
||||
env["SKD_UPDATE_SERVICE_URL"] = settings.update_service_url
|
||||
env["SKD_UPDATE_PROJECT_ID"] = settings.update_project_id
|
||||
env["SKD_UPDATE_TOKEN"] = settings.update_token
|
||||
env["SKD_UPDATE_STATUS_FILE"] = settings.update_status_file
|
||||
env["SKD_UPDATE_LOG_FILE"] = settings.update_log_file
|
||||
subprocess.Popen([str(script_path)], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.Popen(
|
||||
[str(script_path)], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
|
||||
def start_update(settings: Settings, version: str | None = None) -> None:
|
||||
_write_status(settings, "in_progress", version or _read_version())
|
||||
current_version = version or _read_version()
|
||||
_write_status(settings, "in_progress", current_version)
|
||||
report_status(settings, "in_progress", current_version)
|
||||
script = _project_root() / "scripts" / "update_client.sh"
|
||||
_run_async(script, settings)
|
||||
|
||||
|
||||
def start_rollback(settings: Settings) -> None:
|
||||
_write_status(settings, "in_progress", _read_version())
|
||||
current_version = _read_version()
|
||||
_write_status(settings, "in_progress", current_version)
|
||||
report_status(settings, "in_progress", current_version)
|
||||
script = _project_root() / "scripts" / "rollback_client.sh"
|
||||
_run_async(script, settings)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user