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 = ` +
+
+
Deploying Stack
+

Launching your application stack...

+
+ `; + stepsContainer.appendChild(stackDeployStep); } async function startLaunch(data) { @@ -226,6 +238,58 @@ async function startLaunch(data) { }; dockerComposeStep.querySelector('.step-content').appendChild(downloadButton); + // Step 7: Deploy Stack + await updateStep(7, 'Deploying Stack', 'Launching your application stack...'); + const stackResult = await deployStack(dockerComposeResult.content, data.instanceName, data.port); + + if (!stackResult.success) { + throw new Error(stackResult.error || 'Failed to deploy stack'); + } + + // Update the step to show success + const stackDeployStep = document.querySelectorAll('.step-item')[6]; + stackDeployStep.classList.remove('active'); + stackDeployStep.classList.add('completed'); + stackDeployStep.querySelector('.step-status').textContent = 'Successfully deployed stack'; + + // Add stack details + const stackDetails = document.createElement('div'); + stackDetails.className = 'mt-3'; + stackDetails.innerHTML = ` +
+
+
Stack Deployment Results
+
+ + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Stack Name${stackResult.data.name}
Stack ID${stackResult.data.id}
Status + Deployed +
+
+
+
+ `; + stackDeployStep.querySelector('.step-content').appendChild(stackDetails); + } catch (error) { showError(error.message); } @@ -912,4 +976,48 @@ async function downloadDockerCompose(repo, branch) { error: error.message }; } +} + +// Add new function to deploy stack +async function deployStack(dockerComposeContent, stackName, port) { + try { + const response = await fetch('/api/admin/deploy-stack', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + name: `docupulse_${port}`, + StackFileContent: dockerComposeContent, + Env: [ + { + name: 'PORT', + value: port.toString() + }, + { + name: 'ISMASTER', + value: 'false' + } + ] + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to deploy stack'); + } + + const result = await response.json(); + return { + success: true, + data: result + }; + } catch (error) { + console.error('Error deploying stack:', error); + return { + success: false, + error: error.message + }; + } } \ No newline at end of file