gitarsenal-cli 1.1.2 → 1.1.4

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.
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Modal Proxy Service for GitArsenal CLI
4
+
5
+ This service allows GitArsenal CLI users to access Modal services without exposing your Modal token.
6
+ It acts as a secure proxy between clients and Modal's API.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import secrets
12
+ import string
13
+ import time
14
+ from flask import Flask, request, jsonify
15
+ from flask_cors import CORS
16
+ import modal
17
+ import threading
18
+ import logging
19
+ from dotenv import load_dotenv
20
+ import uuid
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ # Add the current directory to the path so we can import the test_modalSandboxScript module
25
+ current_dir = Path(__file__).parent.absolute()
26
+ sys.path.append(str(current_dir.parent))
27
+
28
+ # Load environment variables from .env file
29
+ load_dotenv()
30
+
31
+ # Configure logging
32
+ logging.basicConfig(
33
+ level=logging.INFO,
34
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
35
+ handlers=[
36
+ logging.FileHandler("modal_proxy.log"),
37
+ logging.StreamHandler()
38
+ ]
39
+ )
40
+ logger = logging.getLogger("modal-proxy")
41
+
42
+ app = Flask(__name__)
43
+ CORS(app) # Enable CORS for all routes
44
+
45
+ # Get Modal token from environment variable
46
+ MODAL_TOKEN = os.environ.get("MODAL_TOKEN")
47
+ if not MODAL_TOKEN:
48
+ logger.error("MODAL_TOKEN environment variable is not set!")
49
+
50
+ # Dictionary to store active containers
51
+ active_containers = {}
52
+
53
+ # Authentication tokens for clients
54
+ # In a production environment, use a proper authentication system
55
+ API_KEYS = {}
56
+ if os.environ.get("API_KEYS"):
57
+ API_KEYS = {key.strip(): True for key in os.environ.get("API_KEYS").split(",")}
58
+
59
+ def generate_api_key():
60
+ """Generate a new API key for a client"""
61
+ return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32))
62
+
63
+ def authenticate_request():
64
+ """Authenticate the request using API key"""
65
+ api_key = request.headers.get('X-API-Key')
66
+ if not api_key or api_key not in API_KEYS:
67
+ return False
68
+ return True
69
+
70
+ def generate_random_password(length=16):
71
+ """Generate a random password for SSH access"""
72
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
73
+ password = ''.join(secrets.choice(alphabet) for i in range(length))
74
+ return password
75
+
76
+ def setup_modal_auth():
77
+ """Set up Modal authentication using the server's token"""
78
+ if not MODAL_TOKEN:
79
+ logger.error("Cannot set up Modal authentication: No token provided")
80
+ return False
81
+
82
+ try:
83
+ # Set the token in the environment
84
+ os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
85
+ # Also set the token directly in modal.config
86
+ modal.config._auth_config.token_id = MODAL_TOKEN
87
+ logger.info("Modal token set in environment and config")
88
+ return True
89
+ except Exception as e:
90
+ logger.error(f"Error setting up Modal authentication: {e}")
91
+ return False
92
+
93
+ @app.route('/api/health', methods=['GET'])
94
+ def health_check():
95
+ """Health check endpoint"""
96
+ return jsonify({"status": "ok", "message": "Modal proxy service is running"})
97
+
98
+ @app.route('/api/create-api-key', methods=['POST'])
99
+ def create_api_key():
100
+ """Create a new API key (protected by admin key)"""
101
+ admin_key = request.headers.get('X-Admin-Key')
102
+ if not admin_key or admin_key != os.environ.get("ADMIN_KEY"):
103
+ return jsonify({"error": "Unauthorized"}), 401
104
+
105
+ new_key = generate_api_key()
106
+ API_KEYS[new_key] = True
107
+
108
+ return jsonify({"api_key": new_key})
109
+
110
+ @app.route('/api/create-sandbox', methods=['POST'])
111
+ def create_sandbox():
112
+ """Create a Modal sandbox"""
113
+ if not authenticate_request():
114
+ return jsonify({"error": "Unauthorized"}), 401
115
+
116
+ if not setup_modal_auth():
117
+ return jsonify({"error": "Failed to set up Modal authentication"}), 500
118
+
119
+ try:
120
+ data = request.json
121
+ gpu_type = data.get('gpu_type', 'A10G')
122
+ repo_url = data.get('repo_url')
123
+ repo_name = data.get('repo_name')
124
+ setup_commands = data.get('setup_commands', [])
125
+ volume_name = data.get('volume_name')
126
+
127
+ # Import the sandbox creation function from your module
128
+ from test_modalSandboxScript import create_modal_sandbox
129
+
130
+ logger.info(f"Creating sandbox with GPU: {gpu_type}, Repo: {repo_url}")
131
+
132
+ # Create a unique ID for this sandbox
133
+ sandbox_id = str(uuid.uuid4())
134
+
135
+ # Start sandbox creation in a separate thread
136
+ def create_sandbox_thread():
137
+ try:
138
+ # Ensure Modal token is set before creating sandbox
139
+ if not setup_modal_auth():
140
+ logger.error("Failed to set up Modal authentication in thread")
141
+ return
142
+
143
+ result = create_modal_sandbox(
144
+ gpu_type,
145
+ repo_url=repo_url,
146
+ repo_name=repo_name,
147
+ setup_commands=setup_commands,
148
+ volume_name=volume_name
149
+ )
150
+
151
+ if result:
152
+ active_containers[sandbox_id] = {
153
+ "container_id": result.get("container_id"),
154
+ "sandbox_id": result.get("sandbox_id"),
155
+ "created_at": time.time(),
156
+ "type": "sandbox"
157
+ }
158
+ logger.info(f"Sandbox created successfully: {result.get('container_id')}")
159
+ else:
160
+ logger.error("Failed to create sandbox")
161
+ except Exception as e:
162
+ logger.error(f"Error in sandbox creation thread: {e}")
163
+
164
+ thread = threading.Thread(target=create_sandbox_thread)
165
+ thread.daemon = True
166
+ thread.start()
167
+
168
+ return jsonify({
169
+ "message": "Sandbox creation started",
170
+ "sandbox_id": sandbox_id
171
+ })
172
+
173
+ except Exception as e:
174
+ logger.error(f"Error creating sandbox: {e}")
175
+ return jsonify({"error": str(e)}), 500
176
+
177
+ @app.route('/api/create-ssh-container', methods=['POST'])
178
+ def create_ssh_container():
179
+ """Create a Modal SSH container"""
180
+ if not authenticate_request():
181
+ return jsonify({"error": "Unauthorized"}), 401
182
+
183
+ if not setup_modal_auth():
184
+ return jsonify({"error": "Failed to set up Modal authentication"}), 500
185
+
186
+ try:
187
+ data = request.json
188
+ gpu_type = data.get('gpu_type', 'A10G')
189
+ repo_url = data.get('repo_url')
190
+ repo_name = data.get('repo_name')
191
+ setup_commands = data.get('setup_commands', [])
192
+ volume_name = data.get('volume_name')
193
+ timeout_minutes = data.get('timeout', 60)
194
+
195
+ # Generate a random password for SSH
196
+ ssh_password = generate_random_password()
197
+
198
+ # Import the SSH container creation function
199
+ from test_modalSandboxScript import create_modal_ssh_container
200
+
201
+ logger.info(f"Creating SSH container with GPU: {gpu_type}, Repo: {repo_url}")
202
+
203
+ # Create a unique ID for this container
204
+ container_id = str(uuid.uuid4())
205
+
206
+ # Start container creation in a separate thread
207
+ def create_container_thread():
208
+ try:
209
+ # Ensure Modal token is set before creating container
210
+ if not setup_modal_auth():
211
+ logger.error("Failed to set up Modal authentication in thread")
212
+ return
213
+
214
+ # Explicitly set the Modal token in the environment again for this thread
215
+ os.environ["MODAL_TOKEN_ID"] = MODAL_TOKEN
216
+
217
+ # Log token status for debugging
218
+ token_status = "Token is set" if MODAL_TOKEN else "Token is missing"
219
+ logger.info(f"Modal token status: {token_status}")
220
+
221
+ result = create_modal_ssh_container(
222
+ gpu_type,
223
+ repo_url=repo_url,
224
+ repo_name=repo_name,
225
+ setup_commands=setup_commands,
226
+ volume_name=volume_name,
227
+ timeout_minutes=timeout_minutes,
228
+ ssh_password=ssh_password
229
+ )
230
+
231
+ if result:
232
+ active_containers[container_id] = {
233
+ "container_id": result.get("app_name"),
234
+ "ssh_password": ssh_password,
235
+ "created_at": time.time(),
236
+ "type": "ssh"
237
+ }
238
+ logger.info(f"SSH container created successfully: {result.get('app_name')}")
239
+ else:
240
+ logger.error("Failed to create SSH container")
241
+ except Exception as e:
242
+ logger.error(f"Error in SSH container creation thread: {e}")
243
+
244
+ thread = threading.Thread(target=create_container_thread)
245
+ thread.daemon = True
246
+ thread.start()
247
+
248
+ return jsonify({
249
+ "message": "SSH container creation started",
250
+ "container_id": container_id,
251
+ "ssh_password": ssh_password
252
+ })
253
+
254
+ except Exception as e:
255
+ logger.error(f"Error creating SSH container: {e}")
256
+ return jsonify({"error": str(e)}), 500
257
+
258
+ @app.route('/api/container-status/<container_id>', methods=['GET'])
259
+ def container_status(container_id):
260
+ """Get status of a container"""
261
+ if not authenticate_request():
262
+ return jsonify({"error": "Unauthorized"}), 401
263
+
264
+ if container_id in active_containers:
265
+ # Remove sensitive information like passwords
266
+ container_info = active_containers[container_id].copy()
267
+ if "ssh_password" in container_info:
268
+ del container_info["ssh_password"]
269
+
270
+ return jsonify({
271
+ "status": "active",
272
+ "info": container_info
273
+ })
274
+ else:
275
+ return jsonify({
276
+ "status": "not_found",
277
+ "message": "Container not found or has been terminated"
278
+ }), 404
279
+
280
+ @app.route('/api/terminate-container', methods=['POST'])
281
+ def terminate_container():
282
+ """Terminate a Modal container"""
283
+ if not authenticate_request():
284
+ return jsonify({"error": "Unauthorized"}), 401
285
+
286
+ if not setup_modal_auth():
287
+ return jsonify({"error": "Failed to set up Modal authentication"}), 500
288
+
289
+ try:
290
+ data = request.json
291
+ container_id = data.get('container_id')
292
+
293
+ if not container_id:
294
+ return jsonify({"error": "Container ID is required"}), 400
295
+
296
+ if container_id not in active_containers:
297
+ return jsonify({"error": "Container not found"}), 404
298
+
299
+ modal_container_id = active_containers[container_id].get("container_id")
300
+
301
+ # Terminate the container using Modal CLI
302
+ import subprocess
303
+ result = subprocess.run(
304
+ ["modal", "container", "terminate", modal_container_id],
305
+ capture_output=True,
306
+ text=True
307
+ )
308
+
309
+ if result.returncode == 0:
310
+ # Remove from active containers
311
+ del active_containers[container_id]
312
+ logger.info(f"Container terminated successfully: {modal_container_id}")
313
+ return jsonify({"message": "Container terminated successfully"})
314
+ else:
315
+ logger.error(f"Failed to terminate container: {result.stderr}")
316
+ return jsonify({"error": f"Failed to terminate container: {result.stderr}"}), 500
317
+
318
+ except Exception as e:
319
+ logger.error(f"Error terminating container: {e}")
320
+ return jsonify({"error": str(e)}), 500
321
+
322
+ if __name__ == '__main__':
323
+ # Check if Modal token is set
324
+ if not MODAL_TOKEN:
325
+ logger.error("MODAL_TOKEN environment variable must be set!")
326
+ exit(1)
327
+
328
+ # Generate an admin key if not set
329
+ if not os.environ.get("ADMIN_KEY"):
330
+ admin_key = generate_api_key()
331
+ os.environ["ADMIN_KEY"] = admin_key
332
+ logger.info(f"Generated admin key: {admin_key}")
333
+ print(f"Admin key: {admin_key}")
334
+
335
+ port = int(os.environ.get("PORT", 5001)) # Default to 5001 to avoid macOS AirPlay conflict
336
+ app.run(host='0.0.0.0', port=port)
@@ -1,4 +1,6 @@
1
1
  modal>=0.56.4
2
2
  requests>=2.31.0
3
3
  pathlib>=1.0.1
4
- python-dotenv>=1.0.0
4
+ python-dotenv>=1.0.0
5
+ flask>=2.0.0
6
+ flask-cors>=3.0.0
@@ -2067,6 +2067,38 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2067
2067
  volume_name=None, timeout_minutes=60, ssh_password=None):
2068
2068
  """Create a Modal SSH container with GPU support and tunneling"""
2069
2069
 
2070
+ # Check if Modal is authenticated
2071
+ try:
2072
+ # Try to access Modal token to check authentication
2073
+ try:
2074
+ # This will raise an exception if not authenticated
2075
+ modal.config.get_current_workspace_name()
2076
+ print("āœ… Modal authentication verified")
2077
+ except modal.exception.AuthError:
2078
+ print("\n" + "="*80)
2079
+ print("šŸ”‘ MODAL AUTHENTICATION REQUIRED")
2080
+ print("="*80)
2081
+ print("GitArsenal requires Modal authentication to create cloud environments.")
2082
+
2083
+ # Check if token is in environment
2084
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2085
+ if not modal_token:
2086
+ print("āš ļø No Modal token found in environment.")
2087
+ return None
2088
+ else:
2089
+ print("šŸ”„ Using Modal token from environment")
2090
+ # Try to authenticate with the token
2091
+ try:
2092
+ # Set token directly in modal config
2093
+ modal.config._auth_config.token_id = modal_token
2094
+ print("āœ… Modal token set in config")
2095
+ except Exception as e:
2096
+ print(f"āš ļø Error setting Modal token: {e}")
2097
+ return None
2098
+ except Exception as e:
2099
+ print(f"āš ļø Error checking Modal authentication: {e}")
2100
+ print("Continuing anyway, but Modal operations may fail")
2101
+
2070
2102
  # Generate a unique app name with timestamp to avoid conflicts
2071
2103
  timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
2072
2104
  app_name = f"ssh-container-{timestamp}"
@@ -2116,6 +2148,18 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2116
2148
  print(f"āš ļø Could not create default volume: {e}")
2117
2149
  print("āš ļø Continuing without persistent volume")
2118
2150
  volume = None
2151
+
2152
+ # Print debug info for authentication
2153
+ print("šŸ” Modal authentication debug info:")
2154
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2155
+ print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
2156
+ print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
2157
+
2158
+ try:
2159
+ workspace = modal.config.get_current_workspace_name()
2160
+ print(f" - Current workspace: {workspace}")
2161
+ except Exception as e:
2162
+ print(f" - Error getting workspace: {e}")
2119
2163
 
2120
2164
  # Create SSH-enabled image
2121
2165
  ssh_image = (
@@ -2153,7 +2197,12 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2153
2197
  )
2154
2198
 
2155
2199
  # Create the Modal app
2156
- app = modal.App(app_name)
2200
+ try:
2201
+ app = modal.App(app_name)
2202
+ print("āœ… Created Modal app successfully")
2203
+ except Exception as e:
2204
+ print(f"āŒ Error creating Modal app: {e}")
2205
+ return None
2157
2206
 
2158
2207
  # Configure volumes if available
2159
2208
  volumes_config = {}
@@ -2319,6 +2368,10 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2319
2368
  "volume_name": volume_name,
2320
2369
  "volume_mount_path": volume_mount_path if volume else None
2321
2370
  }
2371
+ except modal.exception.AuthError as auth_err:
2372
+ print(f"āŒ Modal authentication error: {auth_err}")
2373
+ print("šŸ”‘ Please check that your Modal token is valid and properly set")
2374
+ return None
2322
2375
  except KeyboardInterrupt:
2323
2376
  print("\nšŸ‘‹ Container startup interrupted")
2324
2377
  return None