Add complete update mechanism for client-side updates: - backend/update.py: Core update logic (check, apply, rollback, status/logs) - backend/app.py: REST API endpoints (GET /update/status, POST /update/check, POST /update/apply, POST /update/rollback, GET /update/logs) - backend/models.py: Pydantic models for update API responses - backend/settings.py: Update config (status/log file paths) - scripts/rollback_client.sh: Rollback script for failed updates - scripts/update_client.sh: Enhanced update client script - CLAUDE.md: Documentation for future Claude Code instances Complete US_000026-028 and TASK_000027-029: - US_000026: Client pulls updates from remote service - US_000027: Client verifies and applies updates atomically - US_000028: Client reports update status to backend All endpoints require authentication. Updates run asynchronously. Documentation updated per SOP (CHANGELOG, PROJECT_STATUS, stories/tasks). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
70 lines
3.3 KiB
Python
70 lines
3.3 KiB
Python
import os
|
|
from functools import lru_cache
|
|
from typing import List
|
|
|
|
|
|
class Settings:
|
|
"""Application settings loaded from environment."""
|
|
|
|
def __init__(self) -> None:
|
|
self.auth_mode: str = os.getenv("SKD_AUTH_MODE", "pam").lower()
|
|
if self.auth_mode not in ("pam", "oidc"):
|
|
self.auth_mode = "pam"
|
|
self.allowed_users: List[str] = self._parse_list(os.getenv("SKD_ALLOWED_USERS", ""))
|
|
self.auth_secret: str = os.getenv("SKD_AUTH_SECRET", "change-me-secret")
|
|
self.token_ttl_seconds: int = int(os.getenv("SKD_TOKEN_TTL_SECONDS", "900"))
|
|
self.auth_allowed_users: List[str] = self._parse_list(os.getenv("SKD_AUTH_ALLOWED_USERS", ""))
|
|
self.auth_allowed_groups: List[str] = self._parse_list(
|
|
os.getenv("SKD_AUTH_ALLOWED_GROUPS", "sudo")
|
|
)
|
|
self.auth_pam_service: str = os.getenv("SKD_AUTH_PAM_SERVICE", "login")
|
|
self.oidc_issuer: str = os.getenv("SKD_OIDC_ISSUER", "")
|
|
self.oidc_client_id: str = os.getenv("SKD_OIDC_CLIENT_ID", "")
|
|
self.oidc_client_secret: str = os.getenv("SKD_OIDC_CLIENT_SECRET", "")
|
|
self.oidc_redirect_uri: str = os.getenv(
|
|
"SKD_OIDC_REDIRECT_URI", "http://localhost:8000/login/oidc/callback"
|
|
)
|
|
self.oidc_scopes: str = os.getenv("SKD_OIDC_SCOPES", "openid profile email")
|
|
self.session_cookie_name: str = os.getenv("SKD_SESSION_COOKIE_NAME", "skd_session")
|
|
self.session_cookie_secure: bool = (
|
|
os.getenv("SKD_SESSION_COOKIE_SECURE", "false").lower() == "true"
|
|
)
|
|
self.oidc_state_cookie_name: str = os.getenv(
|
|
"SKD_OIDC_STATE_COOKIE_NAME", "skd_oidc_state"
|
|
)
|
|
self.oidc_enabled: bool = bool(
|
|
self.oidc_issuer and self.oidc_client_id and self.oidc_client_secret
|
|
)
|
|
self.default_countdown: int = int(os.getenv("SKD_DEFAULT_COUNTDOWN", "60"))
|
|
self.default_sound: bool = os.getenv("SKD_DEFAULT_SOUND", "false").lower() == "true"
|
|
self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5"))
|
|
self.dry_run: bool = os.getenv("SKD_DRY_RUN", "false").lower() == "true"
|
|
self.update_url: str = os.getenv("SKD_UPDATE_URL", "https://update.wlkns.org")
|
|
self.update_token: str = os.getenv("SKD_UPDATE_TOKEN", "")
|
|
self.update_interval: int = int(os.getenv("SKD_UPDATE_INTERVAL", "3600"))
|
|
self.update_status_url: str = os.getenv(
|
|
"SKD_UPDATE_STATUS_URL", "https://update.wlkns.org/status"
|
|
)
|
|
self.update_status_file: str = os.getenv(
|
|
"SKD_UPDATE_STATUS_FILE", "/var/lib/skd/update_status.json"
|
|
)
|
|
self.update_log_file: str = os.getenv(
|
|
"SKD_UPDATE_LOG_FILE", "/var/lib/skd/update_logs.jsonl"
|
|
)
|
|
# Paths/tools
|
|
self.notify_send_path: str = os.getenv("SKD_NOTIFY_SEND_PATH", "notify-send")
|
|
self.sound_player: str = os.getenv("SKD_SOUND_PLAYER", "paplay")
|
|
self.sound_file: str = os.getenv(
|
|
"SKD_SOUND_FILE",
|
|
"/usr/share/sounds/freedesktop/stereo/dialog-warning.oga",
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse_list(value: str) -> List[str]:
|
|
return [item for item in (part.strip() for part in value.split(",")) if item]
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_settings() -> Settings:
|
|
return Settings()
|