diff --git a/routes/launch_api.py b/routes/launch_api.py index a1b2415..fa32c70 100644 --- a/routes/launch_api.py +++ b/routes/launch_api.py @@ -7,6 +7,7 @@ from datetime import datetime import requests import base64 from routes.admin_api import token_required +import json launch_api = Blueprint('launch_api', __name__) @@ -709,4 +710,135 @@ def download_docker_compose(): except Exception as e: current_app.logger.error(f"Error downloading docker-compose.yml: {str(e)}") - return jsonify({'message': f'Error downloading docker-compose.yml: {str(e)}'}), 500 \ No newline at end of file + return jsonify({'message': f'Error downloading docker-compose.yml: {str(e)}'}), 500 + +@launch_api.route('/deploy-stack', methods=['POST']) +@csrf.exempt +def deploy_stack(): + try: + data = request.get_json() + if not data or 'name' not in data or 'StackFileContent' not in data: + return jsonify({'error': 'Missing required fields'}), 400 + + # Get Portainer settings + portainer_settings = KeyValueSettings.get_value('portainer_settings') + if not portainer_settings: + return jsonify({'error': 'Portainer settings not configured'}), 400 + + # Verify Portainer authentication + auth_response = requests.get( + f"{portainer_settings['url'].rstrip('/')}/api/status", + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + timeout=30 # 30 seconds timeout for status check + ) + + if not auth_response.ok: + current_app.logger.error(f"Portainer authentication failed: {auth_response.text}") + return jsonify({'error': 'Failed to authenticate with Portainer'}), 401 + + # Get Portainer endpoint ID (assuming it's the first endpoint) + endpoint_response = requests.get( + f"{portainer_settings['url'].rstrip('/')}/api/endpoints", + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + timeout=30 # 30 seconds timeout for endpoint check + ) + if not endpoint_response.ok: + error_text = endpoint_response.text + try: + error_json = endpoint_response.json() + error_text = error_json.get('message', error_text) + except: + pass + return jsonify({'error': f'Failed to get Portainer endpoints: {error_text}'}), 500 + + endpoints = endpoint_response.json() + if not endpoints: + return jsonify({'error': 'No Portainer endpoints found'}), 400 + + endpoint_id = endpoints[0]['Id'] + + # Log the request data + current_app.logger.info(f"Creating stack with data: {json.dumps(data)}") + current_app.logger.info(f"Using endpoint ID: {endpoint_id}") + + # First, check if a stack with this name already exists + stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks" + stacks_response = requests.get( + stacks_url, + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Accept': 'application/json' + }, + params={'filters': json.dumps({'Name': data['name']})}, + timeout=30 + ) + + if stacks_response.ok: + existing_stacks = stacks_response.json() + for stack in existing_stacks: + if stack['Name'] == data['name']: + current_app.logger.info(f"Found existing stack: {stack['Name']} (ID: {stack['Id']})") + return jsonify({ + 'name': stack['Name'], + 'id': stack['Id'], + 'status': 'existing' + }) + + # If no existing stack found, proceed with creation + url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/create/standalone/string" + current_app.logger.info(f"Making request to: {url}") + + # Prepare the request body according to Portainer's API spec + request_body = data + + # Add endpointId as a query parameter + params = {'endpointId': endpoint_id} + + # Set a longer timeout for stack creation (10 minutes) + create_response = requests.post( + url, + headers={ + 'X-API-Key': portainer_settings['api_key'], + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + params=params, + json=request_body, + timeout=600 # 10 minutes timeout for stack creation + ) + + # Log the response details + current_app.logger.info(f"Response status: {create_response.status_code}") + current_app.logger.info(f"Response headers: {dict(create_response.headers)}") + + response_text = create_response.text + current_app.logger.info(f"Response body: {response_text}") + + if not create_response.ok: + error_message = response_text + try: + error_json = create_response.json() + error_message = error_json.get('message', error_message) + except: + pass + return jsonify({'error': f'Failed to create stack: {error_message}'}), 500 + + stack_info = create_response.json() + return jsonify({ + 'name': stack_info['Name'], + 'id': stack_info['Id'], + 'status': 'created' + }) + + except requests.exceptions.Timeout: + current_app.logger.error("Request timed out while deploying stack") + return jsonify({'error': 'Request timed out while deploying stack. The operation may still be in progress.'}), 504 + except Exception as e: + current_app.logger.error(f"Error deploying stack: {str(e)}") + return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index 1ff5ed7..7ea297f 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -87,6 +87,18 @@ function initializeSteps() { `; stepsContainer.appendChild(dockerComposeStep); + + // Add Portainer stack deployment step + const stackDeployStep = document.createElement('div'); + stackDeployStep.className = 'step-item'; + stackDeployStep.innerHTML = ` +
+Launching your application stack...
+| Property | +Value | +
|---|---|
| Stack Name | +${stackResult.data.name} | +
| Stack ID | +${stackResult.data.id} | +
| Status | ++ Deployed + | +