deploying works!
This commit is contained in:
@@ -7,6 +7,7 @@ from datetime import datetime
|
|||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
from routes.admin_api import token_required
|
from routes.admin_api import token_required
|
||||||
|
import json
|
||||||
|
|
||||||
launch_api = Blueprint('launch_api', __name__)
|
launch_api = Blueprint('launch_api', __name__)
|
||||||
|
|
||||||
@@ -709,4 +710,135 @@ def download_docker_compose():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error downloading docker-compose.yml: {str(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
|
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
|
||||||
@@ -87,6 +87,18 @@ function initializeSteps() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
stepsContainer.appendChild(dockerComposeStep);
|
stepsContainer.appendChild(dockerComposeStep);
|
||||||
|
|
||||||
|
// Add Portainer stack deployment step
|
||||||
|
const stackDeployStep = document.createElement('div');
|
||||||
|
stackDeployStep.className = 'step-item';
|
||||||
|
stackDeployStep.innerHTML = `
|
||||||
|
<div class="step-icon"><i class="fab fa-docker"></i></div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h5>Deploying Stack</h5>
|
||||||
|
<p class="step-status">Launching your application stack...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
stepsContainer.appendChild(stackDeployStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startLaunch(data) {
|
async function startLaunch(data) {
|
||||||
@@ -226,6 +238,58 @@ async function startLaunch(data) {
|
|||||||
};
|
};
|
||||||
dockerComposeStep.querySelector('.step-content').appendChild(downloadButton);
|
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 = `
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title mb-3">Stack Deployment Results</h6>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Property</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Stack Name</td>
|
||||||
|
<td>${stackResult.data.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Stack ID</td>
|
||||||
|
<td>${stackResult.data.id}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-success">Deployed</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
stackDeployStep.querySelector('.step-content').appendChild(stackDetails);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
}
|
}
|
||||||
@@ -912,4 +976,48 @@ async function downloadDockerCompose(repo, branch) {
|
|||||||
error: error.message
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user