rapidctl 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rapidctl/__init__.py +0 -0
- rapidctl/bootstrap/__init__.py +0 -0
- rapidctl/bootstrap/client.py +104 -0
- rapidctl/bootstrap/connectors/__init__.py +46 -0
- rapidctl/bootstrap/connectors/base.py +72 -0
- rapidctl/bootstrap/connectors/linux.py +97 -0
- rapidctl/bootstrap/connectors/osx.py +230 -0
- rapidctl/bootstrap/state.py +107 -0
- rapidctl/cli/__init__.py +198 -0
- rapidctl/cli/actions.py +200 -0
- rapidctl/cli/main.py +133 -0
- rapidctl/cli/mcp.py +76 -0
- rapidctl/cli/tasks.py +204 -0
- rapidctl/errors/__init__.py +11 -0
- rapidctl/utils/version.py +105 -0
- rapidctl-0.1.0.dist-info/METADATA +257 -0
- rapidctl-0.1.0.dist-info/RECORD +18 -0
- rapidctl-0.1.0.dist-info/WHEEL +4 -0
rapidctl/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
import re
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
import os.path
|
|
5
|
+
import rapidctl.cli.tasks
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from rapidctl.bootstrap.state import StateManager
|
|
11
|
+
|
|
12
|
+
class CtlClient:
|
|
13
|
+
"""
|
|
14
|
+
Class that defines and validates the required data that requires definition
|
|
15
|
+
on the CTL client side of the stack.
|
|
16
|
+
|
|
17
|
+
Once actions using this class are complete the CTL client should be ready
|
|
18
|
+
to connect to the container layer and start command provisioning.
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, state_manager: Optional[StateManager] = None):
|
|
21
|
+
self.container_repo: Optional[str] = None
|
|
22
|
+
self.baseline_version: str = "1.0.0"
|
|
23
|
+
self.client_version: str = "0.0.1"
|
|
24
|
+
self.image_id: Optional[str] = None
|
|
25
|
+
self.command_path: str = "/opt/rapidctl/cmd/"
|
|
26
|
+
self.cli: Optional[Any] = None
|
|
27
|
+
|
|
28
|
+
# Pluggable state manager
|
|
29
|
+
self.state_manager = state_manager or StateManager()
|
|
30
|
+
|
|
31
|
+
# Load persisted version state if available
|
|
32
|
+
self._load_persisted_version()
|
|
33
|
+
|
|
34
|
+
def check_for_updates(self) -> Optional[str]:
|
|
35
|
+
"""Check if a newer container version exists locally."""
|
|
36
|
+
import rapidctl.cli.actions as actions
|
|
37
|
+
|
|
38
|
+
if not self.cli:
|
|
39
|
+
self.connect()
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
newer = actions.find_newer_version(self.cli, self.container_repo, self.baseline_version)
|
|
43
|
+
return newer
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _load_persisted_version(self) -> None:
|
|
50
|
+
"""Attempt to load a pinned version from disk."""
|
|
51
|
+
if self.container_repo:
|
|
52
|
+
pinned = self.state_manager.get_state(f"version_{self.container_repo}")
|
|
53
|
+
if pinned:
|
|
54
|
+
self.baseline_version = pinned
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def container_version(self):
|
|
58
|
+
"""
|
|
59
|
+
Function to provide an view of the container image tag
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
str: Aggregrated version of the container repo path and version
|
|
63
|
+
"""
|
|
64
|
+
return self._container_validator("%s:%s" % (self.container_repo, self.baseline_version))
|
|
65
|
+
|
|
66
|
+
def set_version(self, version: str) -> None:
|
|
67
|
+
"""Update the baseline version with validation."""
|
|
68
|
+
# Validate that the version string is safe
|
|
69
|
+
safe_version = re.sub(r'[^a-zA-Z0-9._-]', '', version)
|
|
70
|
+
if safe_version:
|
|
71
|
+
self.baseline_version = safe_version
|
|
72
|
+
|
|
73
|
+
def get_version(self) -> str:
|
|
74
|
+
"""Return the current baseline version."""
|
|
75
|
+
return self.baseline_version
|
|
76
|
+
|
|
77
|
+
def persist_version(self) -> None:
|
|
78
|
+
"""Persist the current baseline version to disk."""
|
|
79
|
+
if self.container_repo:
|
|
80
|
+
self.state_manager.set_state(f"version_{self.container_repo}", self.baseline_version)
|
|
81
|
+
|
|
82
|
+
def connect(self):
|
|
83
|
+
"""
|
|
84
|
+
Connect to Podman and return the active session.
|
|
85
|
+
"""
|
|
86
|
+
if self.cli is None:
|
|
87
|
+
from rapidctl.cli import PodmanCLI
|
|
88
|
+
self.cli = PodmanCLI()
|
|
89
|
+
self.cli._connect_to_podman()
|
|
90
|
+
return self.cli
|
|
91
|
+
|
|
92
|
+
def _container_validator(self, container_image):
|
|
93
|
+
"""
|
|
94
|
+
Sanitize a container image URL/name to prevent command injection and ensure valid format.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
container_image (str): The container image URL or name to sanitize
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
str: Sanitized container image string or None if invalid
|
|
101
|
+
"""
|
|
102
|
+
import rapidctl.cli.tasks as tasks
|
|
103
|
+
return tasks.sanitize_container_image(container_image)
|
|
104
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connectors module for platform-specific container runtime connections.
|
|
3
|
+
|
|
4
|
+
This module provides platform-specific connectors for different operating systems
|
|
5
|
+
to detect and connect to container runtimes like Podman.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_connector():
|
|
14
|
+
"""
|
|
15
|
+
Get the appropriate connector for the current platform.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Connector instance for the current platform
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
NotImplementedError: If the current platform is not supported
|
|
22
|
+
"""
|
|
23
|
+
system = platform.system()
|
|
24
|
+
|
|
25
|
+
if system == "Darwin":
|
|
26
|
+
from rapidctl.bootstrap.connectors.osx import get_connector as get_osx_connector
|
|
27
|
+
return get_osx_connector()
|
|
28
|
+
elif system == "Linux":
|
|
29
|
+
from rapidctl.bootstrap.connectors.linux import get_connector as get_linux_connector
|
|
30
|
+
return get_linux_connector()
|
|
31
|
+
elif system == "Windows":
|
|
32
|
+
# TODO: Implement Windows connector
|
|
33
|
+
raise NotImplementedError(f"Windows connector not yet implemented")
|
|
34
|
+
else:
|
|
35
|
+
raise NotImplementedError(f"Unsupported platform: {system}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def detect_socket() -> Optional[str]:
|
|
39
|
+
"""
|
|
40
|
+
Detect the container runtime socket for the current platform.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: Socket URI or None if not found
|
|
44
|
+
"""
|
|
45
|
+
connector = get_connector()
|
|
46
|
+
return connector.detect_socket()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseConnector(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for platform-specific Podman connectors.
|
|
11
|
+
Defines the standard interface and common functionality shared
|
|
12
|
+
across the operating systems.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.socket_path: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def detect_socket(self) -> Optional[str]:
|
|
20
|
+
"""
|
|
21
|
+
Detect the Podman socket location for the current platform.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
str: URI to the Podman socket or None if not found
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def setup(self) -> bool:
|
|
30
|
+
"""
|
|
31
|
+
Platform-specific setup routine, checking requirements and
|
|
32
|
+
providing guidance or auto-starting Podman if needed.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
bool: True if Podman is ready and socket is found, False otherwise
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def is_podman_installed(self) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Check if Podman is installed on the system.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
bool: True if Podman executable is found in PATH, False otherwise
|
|
45
|
+
"""
|
|
46
|
+
return shutil.which("podman") is not None
|
|
47
|
+
|
|
48
|
+
def _validate_socket(self, socket_uri: str) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Validate that a socket path exists and is accessible.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
socket_uri: Socket URI (e.g., 'unix:///path/to/socket')
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
bool: True if socket is valid and accessible
|
|
57
|
+
"""
|
|
58
|
+
if socket_uri.startswith("unix://"):
|
|
59
|
+
socket_path = socket_uri[7:]
|
|
60
|
+
else:
|
|
61
|
+
socket_path = socket_uri
|
|
62
|
+
|
|
63
|
+
path = Path(socket_path)
|
|
64
|
+
|
|
65
|
+
if not path.exists():
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Standard check: must be a socket and accessible
|
|
69
|
+
if path.is_socket():
|
|
70
|
+
return os.access(path, os.R_OK | os.W_OK)
|
|
71
|
+
|
|
72
|
+
return False
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Linux Connector for Podman
|
|
4
|
+
|
|
5
|
+
Handles Linux-specific requirements for connecting to Podman:
|
|
6
|
+
- Detects the Podman socket location (rootless and rootful)
|
|
7
|
+
- Validates socket accessibility
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from rapidctl.bootstrap.connectors.base import BaseConnector
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LinuxConnector(BaseConnector):
|
|
18
|
+
"""Connector for Podman on Linux systems."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__()
|
|
22
|
+
|
|
23
|
+
def detect_socket(self) -> Optional[str]:
|
|
24
|
+
"""
|
|
25
|
+
Detect the Podman socket location on Linux.
|
|
26
|
+
|
|
27
|
+
Checks:
|
|
28
|
+
1. Environment variable PODMAN_SOCKET
|
|
29
|
+
2. Rootless socket (XDG_RUNTIME_DIR/podman/podman.sock)
|
|
30
|
+
3. Rootful socket (/run/podman/podman.sock)
|
|
31
|
+
4. User-specific socket fallback (/run/user/UID/podman/podman.sock)
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
# Fast path: return existing valid socket
|
|
35
|
+
if self.socket_path and self._validate_socket(self.socket_path):
|
|
36
|
+
return self.socket_path
|
|
37
|
+
|
|
38
|
+
# 1. Check environment variable
|
|
39
|
+
env_socket = os.environ.get("PODMAN_SOCKET")
|
|
40
|
+
if env_socket:
|
|
41
|
+
if self._validate_socket(env_socket):
|
|
42
|
+
self.socket_path = env_socket
|
|
43
|
+
return env_socket
|
|
44
|
+
|
|
45
|
+
# 2. Check XDG_RUNTIME_DIR (Standard for rootless)
|
|
46
|
+
xdg_runtime = os.environ.get("XDG_RUNTIME_DIR")
|
|
47
|
+
if xdg_runtime:
|
|
48
|
+
path = Path(xdg_runtime) / "podman/podman.sock"
|
|
49
|
+
try:
|
|
50
|
+
if self._validate_socket(f"unix://{path}"):
|
|
51
|
+
self.socket_path = f"unix://{path}"
|
|
52
|
+
return self.socket_path
|
|
53
|
+
except PermissionError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
# 3. Check for specific UID-based path as fallback if XDG_RUNTIME_DIR is not set
|
|
57
|
+
try:
|
|
58
|
+
uid = os.getuid()
|
|
59
|
+
uid_path = Path(f"/run/user/{uid}/podman/podman.sock")
|
|
60
|
+
if uid_path.exists():
|
|
61
|
+
if self._validate_socket(f"unix://{uid_path}"):
|
|
62
|
+
self.socket_path = f"unix://{uid_path}"
|
|
63
|
+
return self.socket_path
|
|
64
|
+
except PermissionError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
# 4. Check rootful socket (requires permissions)
|
|
68
|
+
try:
|
|
69
|
+
rootful_path = Path("/run/podman/podman.sock")
|
|
70
|
+
if rootful_path.exists():
|
|
71
|
+
if self._validate_socket(f"unix://{rootful_path}"):
|
|
72
|
+
self.socket_path = f"unix://{rootful_path}"
|
|
73
|
+
return self.socket_path
|
|
74
|
+
except PermissionError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
except Exception:
|
|
78
|
+
# Catch-all for any other unexpected issues during detection
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def _validate_socket(self, socket_uri: str) -> bool:
|
|
84
|
+
"""Validate that a socket path exists and is accessible."""
|
|
85
|
+
return super()._validate_socket(socket_uri)
|
|
86
|
+
|
|
87
|
+
def setup(self) -> bool:
|
|
88
|
+
"""Basic setup for Linux."""
|
|
89
|
+
if not self.is_podman_installed():
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
return self.detect_socket() is not None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_connector() -> LinuxConnector:
|
|
96
|
+
"""Factory function."""
|
|
97
|
+
return LinuxConnector()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
OSX Connector for Podman
|
|
4
|
+
|
|
5
|
+
This connector handles macOS-specific requirements for connecting to Podman:
|
|
6
|
+
- Detects the Podman socket location
|
|
7
|
+
- Validates socket accessibility
|
|
8
|
+
- Sets up any OS-specific requirements
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from rapidctl.bootstrap.connectors.base import BaseConnector
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class OSXConnector(BaseConnector):
|
|
22
|
+
"""Connector for Podman on macOS systems."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.podman_machine_name: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
def detect_socket(self) -> Optional[str]:
|
|
29
|
+
"""
|
|
30
|
+
Detect the Podman socket location on macOS.
|
|
31
|
+
|
|
32
|
+
Checks multiple common locations in order of preference:
|
|
33
|
+
1. Environment variable PODMAN_SOCKET
|
|
34
|
+
2. User's podman machine socket (~/.local/share/containers/podman/machine/podman.sock)
|
|
35
|
+
3. System docker.sock (often symlinked to podman on macOS)
|
|
36
|
+
4. Podman machine-specific sockets
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
str: URI to the Podman socket (e.g., 'unix:///path/to/socket') or None if not found
|
|
40
|
+
"""
|
|
41
|
+
# Fast path: return existing valid socket
|
|
42
|
+
if self.socket_path and self._validate_socket(self.socket_path):
|
|
43
|
+
return self.socket_path
|
|
44
|
+
# Check environment variable first
|
|
45
|
+
env_socket = os.environ.get("PODMAN_SOCKET")
|
|
46
|
+
if env_socket:
|
|
47
|
+
if self._validate_socket(env_socket):
|
|
48
|
+
self.socket_path = env_socket
|
|
49
|
+
return env_socket
|
|
50
|
+
|
|
51
|
+
# Common socket locations on macOS
|
|
52
|
+
socket_candidates = [
|
|
53
|
+
# Podman machine default socket (symlink)
|
|
54
|
+
Path.home() / ".local/share/containers/podman/machine/podman.sock",
|
|
55
|
+
# System docker.sock (may be symlinked to podman)
|
|
56
|
+
Path("/var/run/docker.sock"),
|
|
57
|
+
# Podman Desktop socket
|
|
58
|
+
Path.home() / ".local/share/containers/podman/podman.sock",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Check each candidate
|
|
62
|
+
for socket_path in socket_candidates:
|
|
63
|
+
if socket_path.exists():
|
|
64
|
+
socket_uri = f"unix://{socket_path}"
|
|
65
|
+
if self._validate_socket(socket_uri):
|
|
66
|
+
self.socket_path = socket_uri
|
|
67
|
+
return socket_uri
|
|
68
|
+
|
|
69
|
+
# Try to find machine-specific sockets
|
|
70
|
+
machine_dir = Path.home() / ".local/share/containers/podman/machine"
|
|
71
|
+
if machine_dir.exists():
|
|
72
|
+
for machine_path in machine_dir.iterdir():
|
|
73
|
+
if machine_path.is_dir():
|
|
74
|
+
socket_path = machine_path / "podman.sock"
|
|
75
|
+
if socket_path.exists():
|
|
76
|
+
socket_uri = f"unix://{socket_path}"
|
|
77
|
+
if self._validate_socket(socket_uri):
|
|
78
|
+
self.podman_machine_name = machine_path.name
|
|
79
|
+
self.socket_path = socket_uri
|
|
80
|
+
return socket_uri
|
|
81
|
+
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def _validate_socket(self, socket_uri: str) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Validate that a socket path exists and is accessible.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
socket_uri: Socket URI (e.g., 'unix:///path/to/socket')
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
bool: True if socket is valid and accessible
|
|
93
|
+
"""
|
|
94
|
+
# Call the base implementation first
|
|
95
|
+
if super()._validate_socket(socket_uri):
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
# The base implementation does not allow symlinks.
|
|
99
|
+
# Check if the fallback is a valid symlink to a socket.
|
|
100
|
+
if socket_uri.startswith("unix://"):
|
|
101
|
+
socket_path = socket_uri[7:]
|
|
102
|
+
else:
|
|
103
|
+
socket_path = socket_uri
|
|
104
|
+
|
|
105
|
+
path = Path(socket_path)
|
|
106
|
+
if path.exists() and path.is_symlink():
|
|
107
|
+
return os.access(path, os.R_OK | os.W_OK)
|
|
108
|
+
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def ensure_podman_running(self) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Check if Podman machine is running, and provide guidance if not.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
bool: True if Podman is running, False otherwise
|
|
117
|
+
"""
|
|
118
|
+
# Fast path: If socket is valid and accessible, Podman is effectively running
|
|
119
|
+
if self.socket_path and self._validate_socket(self.socket_path):
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
# Check if podman command is available
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
["podman", "machine", "list", "--format", "json"],
|
|
126
|
+
capture_output=True,
|
|
127
|
+
text=True,
|
|
128
|
+
timeout=5
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if result.returncode == 0:
|
|
132
|
+
# Parse output to check if any machine is running
|
|
133
|
+
import json
|
|
134
|
+
machines = json.loads(result.stdout)
|
|
135
|
+
running_machines = [m for m in machines if m.get("Running", False)]
|
|
136
|
+
|
|
137
|
+
if running_machines:
|
|
138
|
+
self.podman_machine_name = running_machines[0].get("Name")
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def get_connection_info(self) -> dict:
|
|
147
|
+
"""
|
|
148
|
+
Get connection information for Podman on macOS.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
dict: Connection information including socket path and machine name
|
|
152
|
+
"""
|
|
153
|
+
if not self.socket_path:
|
|
154
|
+
self.detect_socket()
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"socket_path": self.socket_path,
|
|
158
|
+
"machine_name": self.podman_machine_name,
|
|
159
|
+
"platform": "darwin",
|
|
160
|
+
"connector": "osx"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def setup(self) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Set up the OSX connector and validate Podman availability.
|
|
166
|
+
|
|
167
|
+
This method:
|
|
168
|
+
1. Checks if Podman is installed
|
|
169
|
+
2. Validates Podman is running and prompts to start if not
|
|
170
|
+
3. Detects the Podman socket
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
bool: True if setup successful, False otherwise
|
|
174
|
+
"""
|
|
175
|
+
# 1. Check if podman is installed
|
|
176
|
+
if not self.is_podman_installed():
|
|
177
|
+
print("Podman is not installed. Please install it (e.g., using 'brew install podman').")
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
# 2. Check if Podman is running
|
|
181
|
+
if not self.ensure_podman_running():
|
|
182
|
+
# Prompt the user to start the machine
|
|
183
|
+
try:
|
|
184
|
+
import sys
|
|
185
|
+
sys.stdout.flush()
|
|
186
|
+
response = input("Podman machine is not running. Would you like to start it? [Y/n] ")
|
|
187
|
+
if response.lower() in ('', 'y', 'yes'):
|
|
188
|
+
print("Starting Podman machine (this may take a moment)...")
|
|
189
|
+
try:
|
|
190
|
+
# Call podman machine start
|
|
191
|
+
subprocess.run(
|
|
192
|
+
["podman", "machine", "start"],
|
|
193
|
+
check=True,
|
|
194
|
+
capture_output=True,
|
|
195
|
+
text=True
|
|
196
|
+
)
|
|
197
|
+
# Wait a moment for the socket to be fully ready
|
|
198
|
+
time.sleep(2)
|
|
199
|
+
|
|
200
|
+
# Re-verify it's running after start
|
|
201
|
+
if not self.ensure_podman_running():
|
|
202
|
+
print("Warning: Podman machine started but status could not be verified.")
|
|
203
|
+
except subprocess.CalledProcessError as e:
|
|
204
|
+
print(f"Failed to start Podman machine: {e.stderr or e.output}")
|
|
205
|
+
return False
|
|
206
|
+
else:
|
|
207
|
+
print("Podman machine must be running to use this tool.")
|
|
208
|
+
return False
|
|
209
|
+
except (KeyboardInterrupt, EOFError):
|
|
210
|
+
print("\nOperation cancelled.")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
# 3. Detect socket
|
|
214
|
+
socket = self.detect_socket()
|
|
215
|
+
if not socket:
|
|
216
|
+
print("Could not detect Podman socket automatically.")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_connector() -> OSXConnector:
|
|
223
|
+
"""
|
|
224
|
+
Factory function to get an OSX connector instance.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
OSXConnector: Configured OSX connector
|
|
228
|
+
"""
|
|
229
|
+
connector = OSXConnector()
|
|
230
|
+
return connector
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class StateManager:
|
|
10
|
+
"""
|
|
11
|
+
Manages persistent state and cache data for rapidctl.
|
|
12
|
+
Pluggable by design: defaults to ~/.rapidctl/state.json, but can be
|
|
13
|
+
overridden for testing or multiple profiles.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, state_file: Optional[Path] = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the state manager.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
state_file: Path to the JSON state file. Defaults to ~/.rapidctl/state.json
|
|
22
|
+
"""
|
|
23
|
+
self.state_file: Path = state_file or Path.home() / ".rapidctl" / "state.json"
|
|
24
|
+
|
|
25
|
+
def get_state(self, key: str) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Get a value from the state file.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
key: The state key
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Any: The stored value, or None if not found or on error
|
|
34
|
+
"""
|
|
35
|
+
if not self.state_file.exists():
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
with open(self.state_file, 'r') as f:
|
|
40
|
+
state = json.load(f)
|
|
41
|
+
return state.get(key)
|
|
42
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
43
|
+
logger.warning(f"Failed to read state from {self.state_file}: {e}")
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
def set_state(self, key: str, value: Any) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Set a value in the state file.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
key: The state key
|
|
52
|
+
value: The data to store (must be JSON serializable)
|
|
53
|
+
"""
|
|
54
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
state = {}
|
|
56
|
+
|
|
57
|
+
if self.state_file.exists():
|
|
58
|
+
try:
|
|
59
|
+
with open(self.state_file, 'r') as f:
|
|
60
|
+
state = json.load(f)
|
|
61
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
62
|
+
logger.warning(f"Failed to load existing state file, overwriting: {e}")
|
|
63
|
+
# We intentionally don't fail here and just overwrite with empty state dict
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
state[key] = value
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
with open(self.state_file, 'w') as f:
|
|
70
|
+
json.dump(state, f, indent=4)
|
|
71
|
+
except OSError as e:
|
|
72
|
+
logger.warning(f"Failed to write state to {self.state_file}: {e}")
|
|
73
|
+
|
|
74
|
+
def get_cache(self, key: str) -> Any:
|
|
75
|
+
"""
|
|
76
|
+
Get a cached value if it has not expired.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
key: The cache key
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Any: The cached data, or None if missing or expired
|
|
83
|
+
"""
|
|
84
|
+
cache_data = self.get_state(f"cache_{key}")
|
|
85
|
+
if cache_data and isinstance(cache_data, dict):
|
|
86
|
+
timestamp = cache_data.get("timestamp", 0)
|
|
87
|
+
ttl = cache_data.get("ttl", 300) # Default 5 mins
|
|
88
|
+
|
|
89
|
+
if time.time() - timestamp < ttl:
|
|
90
|
+
return cache_data.get("data")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def set_cache(self, key: str, data: Any, ttl: int = 300) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Set a cached value with a time-to-live.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
key: The cache key
|
|
99
|
+
data: The data to cache
|
|
100
|
+
ttl: Time to live in seconds (default: 300)
|
|
101
|
+
"""
|
|
102
|
+
cache_data = {
|
|
103
|
+
"timestamp": time.time(),
|
|
104
|
+
"ttl": ttl,
|
|
105
|
+
"data": data
|
|
106
|
+
}
|
|
107
|
+
self.set_state(f"cache_{key}", cache_data)
|