Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 77032062a1 | |||
| 81675af837 | |||
| 0a2cddf122 |
Binary file not shown.
@@ -929,8 +929,8 @@ def deploy_stack():
|
|||||||
def check_stack_status():
|
def check_stack_status():
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data or 'stack_name' not in data:
|
if not data or ('stack_name' not in data and 'stack_id' not in data):
|
||||||
return jsonify({'error': 'Missing stack_name field'}), 400
|
return jsonify({'error': 'Missing stack_name or stack_id field'}), 400
|
||||||
|
|
||||||
# Get Portainer settings
|
# Get Portainer settings
|
||||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||||
@@ -956,7 +956,26 @@ def check_stack_status():
|
|||||||
|
|
||||||
endpoint_id = endpoints[0]['Id']
|
endpoint_id = endpoints[0]['Id']
|
||||||
|
|
||||||
# Get stack information
|
# Get stack information - support both stack_name and stack_id
|
||||||
|
if 'stack_id' in data:
|
||||||
|
# Get stack by ID
|
||||||
|
stack_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}"
|
||||||
|
stack_response = requests.get(
|
||||||
|
stack_url,
|
||||||
|
headers={
|
||||||
|
'X-API-Key': portainer_settings['api_key'],
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
params={'endpointId': endpoint_id},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if not stack_response.ok:
|
||||||
|
return jsonify({'error': f'Stack with ID {data["stack_id"]} not found'}), 404
|
||||||
|
|
||||||
|
target_stack = stack_response.json()
|
||||||
|
else:
|
||||||
|
# Get stack by name (existing logic)
|
||||||
stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks"
|
stacks_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks"
|
||||||
stacks_response = requests.get(
|
stacks_response = requests.get(
|
||||||
stacks_url,
|
stacks_url,
|
||||||
@@ -984,7 +1003,7 @@ def check_stack_status():
|
|||||||
|
|
||||||
# Get stack services to check their status
|
# Get stack services to check their status
|
||||||
services_url = f"{portainer_settings['url'].rstrip('/')}/api/endpoints/{endpoint_id}/docker/services"
|
services_url = f"{portainer_settings['url'].rstrip('/')}/api/endpoints/{endpoint_id}/docker/services"
|
||||||
current_app.logger.info(f"Checking services for stack {data['stack_name']} at endpoint {endpoint_id}")
|
current_app.logger.info(f"Checking services for stack {target_stack['Name']} at endpoint {endpoint_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
services_response = requests.get(
|
services_response = requests.get(
|
||||||
@@ -993,7 +1012,7 @@ def check_stack_status():
|
|||||||
'X-API-Key': portainer_settings['api_key'],
|
'X-API-Key': portainer_settings['api_key'],
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
},
|
},
|
||||||
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={data["stack_name"]}'})},
|
params={'filters': json.dumps({'label': f'com.docker.stack.namespace={target_stack["Name"]}'})},
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1001,46 +1020,40 @@ def check_stack_status():
|
|||||||
|
|
||||||
if services_response.ok:
|
if services_response.ok:
|
||||||
services = services_response.json()
|
services = services_response.json()
|
||||||
current_app.logger.info(f"Found {len(services)} services for stack {data['stack_name']}")
|
|
||||||
|
|
||||||
# Check if all services are running
|
|
||||||
all_running = True
|
|
||||||
service_statuses = []
|
service_statuses = []
|
||||||
|
|
||||||
for service in services:
|
for service in services:
|
||||||
replicas_running = service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0)
|
service_statuses.append({
|
||||||
replicas_actual = service.get('ServiceStatus', {}).get('RunningTasks', 0)
|
|
||||||
|
|
||||||
service_status = {
|
|
||||||
'name': service.get('Spec', {}).get('Name', 'Unknown'),
|
'name': service.get('Spec', {}).get('Name', 'Unknown'),
|
||||||
'replicas_expected': replicas_running,
|
'replicas': service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0),
|
||||||
'replicas_running': replicas_actual,
|
'running_replicas': service.get('ServiceStatus', {}).get('RunningTasks', 0),
|
||||||
'status': 'running' if replicas_actual >= replicas_running else 'not_running'
|
'desired_replicas': service.get('ServiceStatus', {}).get('DesiredTasks', 0)
|
||||||
}
|
})
|
||||||
|
|
||||||
service_statuses.append(service_status)
|
|
||||||
|
|
||||||
if replicas_actual < replicas_running:
|
|
||||||
all_running = False
|
|
||||||
|
|
||||||
# Determine overall stack status
|
# Determine overall stack status
|
||||||
if all_running and len(services) > 0:
|
if not service_statuses:
|
||||||
|
status = 'starting' # No services found yet
|
||||||
|
else:
|
||||||
|
all_running = all(s['running_replicas'] >= s['desired_replicas'] for s in service_statuses if s['desired_replicas'] > 0)
|
||||||
|
any_running = any(s['running_replicas'] > 0 for s in service_statuses)
|
||||||
|
|
||||||
|
if all_running:
|
||||||
status = 'active'
|
status = 'active'
|
||||||
elif len(services) > 0:
|
elif any_running:
|
||||||
status = 'partial'
|
status = 'partial'
|
||||||
else:
|
else:
|
||||||
status = 'inactive'
|
status = 'inactive'
|
||||||
else:
|
else:
|
||||||
# Services API failed, but stack exists - assume it's still starting up
|
# Services API failed, but stack exists - assume it's still starting up
|
||||||
current_app.logger.warning(f"Failed to get services for stack {data['stack_name']}: {services_response.status_code} - {services_response.text}")
|
current_app.logger.warning(f"Failed to get services for stack {target_stack['Name']}: {services_response.status_code} - {services_response.text}")
|
||||||
|
|
||||||
# Provide more specific error context
|
# Provide more specific error context
|
||||||
if services_response.status_code == 404:
|
if services_response.status_code == 404:
|
||||||
current_app.logger.info(f"Services endpoint not found for stack {data['stack_name']} - stack may still be initializing")
|
current_app.logger.info(f"Services endpoint not found for stack {target_stack['Name']} - stack may still be initializing")
|
||||||
elif services_response.status_code == 403:
|
elif services_response.status_code == 403:
|
||||||
current_app.logger.warning(f"Access denied to services for stack {data['stack_name']} - check Portainer permissions")
|
current_app.logger.warning(f"Access denied to services for stack {target_stack['Name']} - check Portainer permissions")
|
||||||
elif services_response.status_code >= 500:
|
elif services_response.status_code >= 500:
|
||||||
current_app.logger.warning(f"Portainer server error when getting services for stack {data['stack_name']}")
|
current_app.logger.warning(f"Portainer server error when getting services for stack {target_stack['Name']}")
|
||||||
|
|
||||||
services = []
|
services = []
|
||||||
service_statuses = []
|
service_statuses = []
|
||||||
@@ -1048,7 +1061,7 @@ def check_stack_status():
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Exception occurred while getting services, but stack exists
|
# Exception occurred while getting services, but stack exists
|
||||||
current_app.logger.warning(f"Exception getting services for stack {data['stack_name']}: {str(e)}")
|
current_app.logger.warning(f"Exception getting services for stack {target_stack['Name']}: {str(e)}")
|
||||||
services = []
|
services = []
|
||||||
service_statuses = []
|
service_statuses = []
|
||||||
status = 'starting' # Stack exists but services not available yet
|
status = 'starting' # Stack exists but services not available yet
|
||||||
@@ -1056,14 +1069,10 @@ def check_stack_status():
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'data': {
|
'data': {
|
||||||
'stack_name': data['stack_name'],
|
'name': target_stack['Name'],
|
||||||
'stack_id': target_stack['Id'],
|
'stack_id': target_stack['Id'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'services': service_statuses,
|
'services': service_statuses
|
||||||
'total_services': len(services),
|
|
||||||
'running_services': len([s for s in service_statuses if s['status'] == 'running']),
|
|
||||||
'stack_created_at': target_stack.get('CreatedAt', 'unknown'),
|
|
||||||
'stack_updated_at': target_stack.get('UpdatedAt', 'unknown')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1855,3 +1864,147 @@ def copy_smtp_settings():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error copying SMTP settings: {str(e)}")
|
current_app.logger.error(f"Error copying SMTP settings: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@launch_api.route('/update-stack', methods=['POST'])
|
||||||
|
@csrf.exempt
|
||||||
|
def update_stack():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not data or 'stack_id' 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
|
||||||
|
|
||||||
|
# Define timeout early to ensure it's available throughout the function
|
||||||
|
stack_timeout = current_app.config.get('STACK_DEPLOYMENT_TIMEOUT', 300) # Default to 5 minutes
|
||||||
|
|
||||||
|
# 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"Updating stack with ID: {data['stack_id']}")
|
||||||
|
current_app.logger.info(f"Using endpoint ID: {endpoint_id}")
|
||||||
|
current_app.logger.info(f"Using timeout: {stack_timeout} seconds")
|
||||||
|
|
||||||
|
# First, verify the stack exists
|
||||||
|
stack_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}"
|
||||||
|
stack_response = requests.get(
|
||||||
|
stack_url,
|
||||||
|
headers={
|
||||||
|
'X-API-Key': portainer_settings['api_key'],
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
params={'endpointId': endpoint_id},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if not stack_response.ok:
|
||||||
|
return jsonify({'error': f'Stack with ID {data["stack_id"]} not found'}), 404
|
||||||
|
|
||||||
|
stack_info = stack_response.json()
|
||||||
|
current_app.logger.info(f"Found existing stack: {stack_info['Name']} (ID: {stack_info['Id']})")
|
||||||
|
|
||||||
|
# Update the stack using Portainer's update API
|
||||||
|
update_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}/update"
|
||||||
|
current_app.logger.info(f"Making update request to: {update_url}")
|
||||||
|
|
||||||
|
# Prepare the request body for stack update
|
||||||
|
request_body = {
|
||||||
|
'StackFileContent': data['StackFileContent'],
|
||||||
|
'Env': data.get('Env', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add endpointId as a query parameter
|
||||||
|
params = {'endpointId': endpoint_id}
|
||||||
|
|
||||||
|
# Use a configurable timeout for stack update initiation
|
||||||
|
update_response = requests.put(
|
||||||
|
update_url,
|
||||||
|
headers={
|
||||||
|
'X-API-Key': portainer_settings['api_key'],
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
params=params,
|
||||||
|
json=request_body,
|
||||||
|
timeout=stack_timeout # Use configurable timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log the response details
|
||||||
|
current_app.logger.info(f"Update response status: {update_response.status_code}")
|
||||||
|
current_app.logger.info(f"Update response headers: {dict(update_response.headers)}")
|
||||||
|
|
||||||
|
response_text = update_response.text
|
||||||
|
current_app.logger.info(f"Update response body: {response_text}")
|
||||||
|
|
||||||
|
if not update_response.ok:
|
||||||
|
error_message = response_text
|
||||||
|
try:
|
||||||
|
error_json = update_response.json()
|
||||||
|
error_message = error_json.get('message', error_message)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return jsonify({'error': f'Failed to update stack: {error_message}'}), 500
|
||||||
|
|
||||||
|
# Stack update initiated successfully
|
||||||
|
current_app.logger.info(f"Stack update initiated: {stack_info['Name']} (ID: {stack_info['Id']})")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'name': stack_info['Name'],
|
||||||
|
'id': stack_info['Id'],
|
||||||
|
'status': 'updating'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
current_app.logger.error(f"Request timed out after {stack_timeout} seconds while initiating stack update")
|
||||||
|
current_app.logger.error(f"Stack ID: {data.get('stack_id', 'unknown') if 'data' in locals() else 'unknown'}")
|
||||||
|
current_app.logger.error(f"Portainer URL: {portainer_settings.get('url', 'unknown') if 'portainer_settings' in locals() else 'unknown'}")
|
||||||
|
return jsonify({
|
||||||
|
'error': f'Request timed out after {stack_timeout} seconds while initiating stack update. The operation may still be in progress.',
|
||||||
|
'timeout_seconds': stack_timeout,
|
||||||
|
'stack_id': data.get('stack_id', 'unknown') if 'data' in locals() else 'unknown'
|
||||||
|
}), 504
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error updating stack: {str(e)}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
@@ -774,6 +774,32 @@ def init_routes(main_bp):
|
|||||||
|
|
||||||
return render_template('main/instance_detail.html', instance=instance)
|
return render_template('main/instance_detail.html', instance=instance)
|
||||||
|
|
||||||
|
@main_bp.route('/api/instances/<int:instance_id>')
|
||||||
|
@login_required
|
||||||
|
@require_password_change
|
||||||
|
def get_instance_data(instance_id):
|
||||||
|
if not os.environ.get('MASTER', 'false').lower() == 'true':
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
instance = Instance.query.get_or_404(instance_id)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'instance': {
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'company': instance.company,
|
||||||
|
'main_url': instance.main_url,
|
||||||
|
'status': instance.status,
|
||||||
|
'payment_plan': instance.payment_plan,
|
||||||
|
'portainer_stack_id': instance.portainer_stack_id,
|
||||||
|
'portainer_stack_name': instance.portainer_stack_name,
|
||||||
|
'deployed_version': instance.deployed_version,
|
||||||
|
'deployed_branch': instance.deployed_branch,
|
||||||
|
'connection_token': instance.connection_token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
@main_bp.route('/instances/<int:instance_id>/auth-status')
|
@main_bp.route('/instances/<int:instance_id>/auth-status')
|
||||||
@login_required
|
@login_required
|
||||||
@require_password_change
|
@require_password_change
|
||||||
@@ -2139,6 +2165,12 @@ def init_routes(main_bp):
|
|||||||
flash('This page is only available in master instances.', 'error')
|
flash('This page is only available in master instances.', 'error')
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
# Get update parameters if this is an update operation
|
||||||
|
is_update = request.args.get('update', 'false').lower() == 'true'
|
||||||
|
instance_id = request.args.get('instance_id')
|
||||||
|
repo_id = request.args.get('repo')
|
||||||
|
branch = request.args.get('branch')
|
||||||
|
|
||||||
# Get NGINX settings
|
# Get NGINX settings
|
||||||
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
||||||
# Get Portainer settings
|
# Get Portainer settings
|
||||||
@@ -2149,7 +2181,11 @@ def init_routes(main_bp):
|
|||||||
return render_template('main/launch_progress.html',
|
return render_template('main/launch_progress.html',
|
||||||
nginx_settings=nginx_settings,
|
nginx_settings=nginx_settings,
|
||||||
portainer_settings=portainer_settings,
|
portainer_settings=portainer_settings,
|
||||||
cloudflare_settings=cloudflare_settings)
|
cloudflare_settings=cloudflare_settings,
|
||||||
|
is_update=is_update,
|
||||||
|
instance_id=instance_id,
|
||||||
|
repo_id=repo_id,
|
||||||
|
branch=branch)
|
||||||
|
|
||||||
@main_bp.route('/api/check-dns', methods=['POST'])
|
@main_bp.route('/api/check-dns', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ let editInstanceModal;
|
|||||||
let addExistingInstanceModal;
|
let addExistingInstanceModal;
|
||||||
let authModal;
|
let authModal;
|
||||||
let launchStepsModal;
|
let launchStepsModal;
|
||||||
|
let updateInstanceModal;
|
||||||
let currentStep = 1;
|
let currentStep = 1;
|
||||||
|
|
||||||
// Update the total number of steps
|
// Update the total number of steps
|
||||||
@@ -15,6 +16,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
|
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
|
||||||
authModal = new bootstrap.Modal(document.getElementById('authModal'));
|
authModal = new bootstrap.Modal(document.getElementById('authModal'));
|
||||||
launchStepsModal = new bootstrap.Modal(document.getElementById('launchStepsModal'));
|
launchStepsModal = new bootstrap.Modal(document.getElementById('launchStepsModal'));
|
||||||
|
updateInstanceModal = new bootstrap.Modal(document.getElementById('updateInstanceModal'));
|
||||||
|
|
||||||
// Initialize tooltips
|
// Initialize tooltips
|
||||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
@@ -1775,3 +1777,167 @@ async function confirmDeleteInstance() {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Instance Functions
|
||||||
|
function showUpdateInstanceModal(instanceId, stackName, instanceUrl) {
|
||||||
|
document.getElementById('update_instance_id').value = instanceId;
|
||||||
|
document.getElementById('update_stack_name').value = stackName;
|
||||||
|
document.getElementById('update_instance_url').value = instanceUrl;
|
||||||
|
|
||||||
|
// Load repositories for the update modal
|
||||||
|
loadUpdateRepositories();
|
||||||
|
|
||||||
|
updateInstanceModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUpdateRepositories() {
|
||||||
|
const repoSelect = document.getElementById('updateRepoSelect');
|
||||||
|
const branchSelect = document.getElementById('updateBranchSelect');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reset branch select
|
||||||
|
branchSelect.innerHTML = '<option value="">Select a repository first</option>';
|
||||||
|
branchSelect.disabled = true;
|
||||||
|
|
||||||
|
const gitSettings = window.gitSettings || {};
|
||||||
|
if (!gitSettings.url || !gitSettings.token) {
|
||||||
|
throw new Error('No Git settings found. Please configure Git in the settings page.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load repositories using the correct existing endpoint
|
||||||
|
const repoResponse = await fetch('/api/admin/list-gitea-repos', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: gitSettings.url,
|
||||||
|
token: gitSettings.token
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!repoResponse.ok) {
|
||||||
|
throw new Error('Failed to load repositories');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await repoResponse.json();
|
||||||
|
|
||||||
|
if (data.repositories && data.repositories.length > 0) {
|
||||||
|
repoSelect.innerHTML = '<option value="">Select a repository</option>' +
|
||||||
|
data.repositories.map(repo =>
|
||||||
|
`<option value="${repo.full_name}" ${repo.full_name === gitSettings.repo ? 'selected' : ''}>${repo.full_name}</option>`
|
||||||
|
).join('');
|
||||||
|
repoSelect.disabled = false;
|
||||||
|
|
||||||
|
// If we have a saved repository, load its branches
|
||||||
|
if (gitSettings.repo) {
|
||||||
|
loadUpdateBranches(gitSettings.repo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
repoSelect.innerHTML = '<option value="">No repositories found</option>';
|
||||||
|
repoSelect.disabled = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading repositories for update:', error);
|
||||||
|
repoSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
|
||||||
|
repoSelect.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUpdateBranches(repoId) {
|
||||||
|
const branchSelect = document.getElementById('updateBranchSelect');
|
||||||
|
|
||||||
|
if (!repoId) {
|
||||||
|
branchSelect.innerHTML = '<option value="">Select a repository first</option>';
|
||||||
|
branchSelect.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gitSettings = window.gitSettings || {};
|
||||||
|
if (!gitSettings.url || !gitSettings.token) {
|
||||||
|
throw new Error('No Git settings found. Please configure Git in the settings page.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/admin/list-gitea-branches', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: gitSettings.url,
|
||||||
|
token: gitSettings.token,
|
||||||
|
repo: repoId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load branches');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.branches && data.branches.length > 0) {
|
||||||
|
branchSelect.innerHTML = '<option value="">Select a branch</option>' +
|
||||||
|
data.branches.map(branch =>
|
||||||
|
`<option value="${branch.name}" ${branch.name === 'master' ? 'selected' : ''}>${branch.name}</option>`
|
||||||
|
).join('');
|
||||||
|
branchSelect.disabled = false;
|
||||||
|
} else {
|
||||||
|
branchSelect.innerHTML = '<option value="">No branches found</option>';
|
||||||
|
branchSelect.disabled = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading branches for update:', error);
|
||||||
|
branchSelect.innerHTML = `<option value="">Error: ${error.message}</option>`;
|
||||||
|
branchSelect.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startInstanceUpdate() {
|
||||||
|
const instanceId = document.getElementById('update_instance_id').value;
|
||||||
|
const stackName = document.getElementById('update_stack_name').value;
|
||||||
|
const instanceUrl = document.getElementById('update_instance_url').value;
|
||||||
|
const repoId = document.getElementById('updateRepoSelect').value;
|
||||||
|
const branch = document.getElementById('updateBranchSelect').value;
|
||||||
|
|
||||||
|
if (!repoId || !branch) {
|
||||||
|
alert('Please select both a repository and a branch.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Store update data in sessionStorage for the launch progress page
|
||||||
|
const updateData = {
|
||||||
|
instanceId: instanceId,
|
||||||
|
stackName: stackName,
|
||||||
|
instanceUrl: instanceUrl,
|
||||||
|
repository: repoId,
|
||||||
|
branch: branch,
|
||||||
|
isUpdate: true
|
||||||
|
};
|
||||||
|
sessionStorage.setItem('instanceUpdateData', JSON.stringify(updateData));
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
updateInstanceModal.hide();
|
||||||
|
|
||||||
|
// Redirect to launch progress page with update parameters
|
||||||
|
window.location.href = `/instances/launch-progress?update=true&instance_id=${instanceId}&repo=${repoId}&branch=${encodeURIComponent(branch)}`;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting instance update:', error);
|
||||||
|
alert('Error starting update: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for update modal
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const updateRepoSelect = document.getElementById('updateRepoSelect');
|
||||||
|
if (updateRepoSelect) {
|
||||||
|
updateRepoSelect.addEventListener('change', function() {
|
||||||
|
loadUpdateBranches(this.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Get the launch data from sessionStorage
|
// Check if this is an update operation
|
||||||
|
if (window.isUpdate && window.updateInstanceId && window.updateRepoId && window.updateBranch) {
|
||||||
|
// This is an update operation
|
||||||
|
const updateData = {
|
||||||
|
instanceId: window.updateInstanceId,
|
||||||
|
repository: window.updateRepoId,
|
||||||
|
branch: window.updateBranch,
|
||||||
|
isUpdate: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the steps
|
||||||
|
initializeSteps();
|
||||||
|
|
||||||
|
// Start the update process
|
||||||
|
startUpdate(updateData);
|
||||||
|
} else {
|
||||||
|
// This is a new launch operation
|
||||||
const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData'));
|
const launchData = JSON.parse(sessionStorage.getItem('instanceLaunchData'));
|
||||||
if (!launchData) {
|
if (!launchData) {
|
||||||
showError('No launch data found. Please start over.');
|
showError('No launch data found. Please start over.');
|
||||||
@@ -11,11 +27,38 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Start the launch process
|
// Start the launch process
|
||||||
startLaunch(launchData);
|
startLaunch(launchData);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function initializeSteps() {
|
function initializeSteps() {
|
||||||
const stepsContainer = document.getElementById('stepsContainer');
|
const stepsContainer = document.getElementById('stepsContainer');
|
||||||
|
const isUpdate = window.isUpdate;
|
||||||
|
|
||||||
|
if (isUpdate) {
|
||||||
|
// For updates, show fewer steps
|
||||||
|
const steps = [
|
||||||
|
{ icon: 'fab fa-docker', title: 'Checking Portainer Connection', description: 'Verifying connection to Portainer...' },
|
||||||
|
{ icon: 'fas fa-file-code', title: 'Downloading Docker Compose', description: 'Fetching docker-compose.yml from repository...' },
|
||||||
|
{ icon: 'fab fa-docker', title: 'Deploying Updated Stack', description: 'Deploying the updated application stack...' },
|
||||||
|
{ icon: 'fas fa-save', title: 'Updating Instance Data', description: 'Updating instance information...' },
|
||||||
|
{ icon: 'fas fa-heartbeat', title: 'Health Check', description: 'Verifying updated instance health...' },
|
||||||
|
{ icon: 'fas fa-check-circle', title: 'Update Complete', description: 'Instance has been successfully updated!' }
|
||||||
|
];
|
||||||
|
|
||||||
|
steps.forEach((step, index) => {
|
||||||
|
const stepElement = document.createElement('div');
|
||||||
|
stepElement.className = 'step-item';
|
||||||
|
stepElement.innerHTML = `
|
||||||
|
<div class="step-icon"><i class="${step.icon}"></i></div>
|
||||||
|
<div class="step-content">
|
||||||
|
<h5>${step.title}</h5>
|
||||||
|
<p class="step-status">${step.description}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
stepsContainer.appendChild(stepElement);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For new launches, show all steps
|
||||||
// Add Cloudflare connection check step
|
// Add Cloudflare connection check step
|
||||||
const cloudflareStep = document.createElement('div');
|
const cloudflareStep = document.createElement('div');
|
||||||
cloudflareStep.className = 'step-item';
|
cloudflareStep.className = 'step-item';
|
||||||
@@ -200,7 +243,7 @@ function initializeSteps() {
|
|||||||
const smtpStep = document.createElement('div');
|
const smtpStep = document.createElement('div');
|
||||||
smtpStep.className = 'step-item';
|
smtpStep.className = 'step-item';
|
||||||
smtpStep.innerHTML = `
|
smtpStep.innerHTML = `
|
||||||
<div class="step-icon"><i class="fas fa-envelope-open"></i></div>
|
<div class="step-icon"><i class="fas fa-envelope"></i></div>
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<h5>Copy SMTP Settings</h5>
|
<h5>Copy SMTP Settings</h5>
|
||||||
<p class="step-status">Configuring email settings...</p>
|
<p class="step-status">Configuring email settings...</p>
|
||||||
@@ -212,19 +255,19 @@ function initializeSteps() {
|
|||||||
const emailStep = document.createElement('div');
|
const emailStep = document.createElement('div');
|
||||||
emailStep.className = 'step-item';
|
emailStep.className = 'step-item';
|
||||||
emailStep.innerHTML = `
|
emailStep.innerHTML = `
|
||||||
<div class="step-icon"><i class="fas fa-envelope"></i></div>
|
<div class="step-icon"><i class="fas fa-paper-plane"></i></div>
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<h5>Send Completion Email</h5>
|
<h5>Send Completion Email</h5>
|
||||||
<p class="step-status">Sending notification to client...</p>
|
<p class="step-status">Sending completion notification...</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
stepsContainer.appendChild(emailStep);
|
stepsContainer.appendChild(emailStep);
|
||||||
|
|
||||||
// Add Download Launch Report step
|
// Add Download Report step
|
||||||
const reportStep = document.createElement('div');
|
const reportStep = document.createElement('div');
|
||||||
reportStep.className = 'step-item';
|
reportStep.className = 'step-item';
|
||||||
reportStep.innerHTML = `
|
reportStep.innerHTML = `
|
||||||
<div class="step-icon"><i class="fas fa-file-download"></i></div>
|
<div class="step-icon"><i class="fas fa-download"></i></div>
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<h5>Download Launch Report</h5>
|
<h5>Download Launch Report</h5>
|
||||||
<p class="step-status">Preparing launch report...</p>
|
<p class="step-status">Preparing launch report...</p>
|
||||||
@@ -232,6 +275,7 @@ function initializeSteps() {
|
|||||||
`;
|
`;
|
||||||
stepsContainer.appendChild(reportStep);
|
stepsContainer.appendChild(reportStep);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function startLaunch(data) {
|
async function startLaunch(data) {
|
||||||
const launchReport = {
|
const launchReport = {
|
||||||
@@ -467,7 +511,28 @@ async function startLaunch(data) {
|
|||||||
downloadButton.className = 'btn btn-sm btn-primary mt-2';
|
downloadButton.className = 'btn btn-sm btn-primary mt-2';
|
||||||
downloadButton.innerHTML = '<i class="fas fa-download me-1"></i> Download docker-compose.yml';
|
downloadButton.innerHTML = '<i class="fas fa-download me-1"></i> Download docker-compose.yml';
|
||||||
downloadButton.onclick = () => {
|
downloadButton.onclick = () => {
|
||||||
const blob = new Blob([dockerComposeResult.content], { type: 'text/yaml' });
|
// Generate the modified docker-compose content with updated volume names
|
||||||
|
let modifiedContent = dockerComposeResult.content;
|
||||||
|
const stackName = generateStackName(data.port);
|
||||||
|
|
||||||
|
// Extract timestamp from stack name (format: docupulse_PORT_TIMESTAMP)
|
||||||
|
const stackNameParts = stackName.split('_');
|
||||||
|
if (stackNameParts.length >= 3) {
|
||||||
|
const timestamp = stackNameParts.slice(2).join('_'); // Get everything after port
|
||||||
|
const baseName = `docupulse_${data.port}_${timestamp}`;
|
||||||
|
|
||||||
|
// Replace volume names to match stack naming convention
|
||||||
|
modifiedContent = modifiedContent.replace(
|
||||||
|
/name: docupulse_\$\{PORT:-10335\}_postgres_data/g,
|
||||||
|
`name: ${baseName}_postgres_data`
|
||||||
|
);
|
||||||
|
modifiedContent = modifiedContent.replace(
|
||||||
|
/name: docupulse_\$\{PORT:-10335\}_uploads/g,
|
||||||
|
`name: ${baseName}_uploads`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([modifiedContent], { type: 'text/yaml' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
@@ -510,7 +575,11 @@ async function startLaunch(data) {
|
|||||||
// Handle different stack deployment scenarios
|
// Handle different stack deployment scenarios
|
||||||
if (!stackResult.success) {
|
if (!stackResult.success) {
|
||||||
// Check if this is a timeout but the stack might still be deploying
|
// Check if this is a timeout but the stack might still be deploying
|
||||||
if (stackResult.error && stackResult.error.includes('timed out')) {
|
if (stackResult.error && (
|
||||||
|
stackResult.error.includes('timed out') ||
|
||||||
|
stackResult.error.includes('504 Gateway Time-out') ||
|
||||||
|
stackResult.error.includes('504 Gateway Timeout')
|
||||||
|
)) {
|
||||||
console.log('Stack deployment timed out, but may still be in progress');
|
console.log('Stack deployment timed out, but may still be in progress');
|
||||||
|
|
||||||
// Update the step to show warning instead of error
|
// Update the step to show warning instead of error
|
||||||
@@ -554,6 +623,19 @@ async function startLaunch(data) {
|
|||||||
// Add stack details
|
// Add stack details
|
||||||
const stackDetails = document.createElement('div');
|
const stackDetails = document.createElement('div');
|
||||||
stackDetails.className = 'mt-3';
|
stackDetails.className = 'mt-3';
|
||||||
|
|
||||||
|
// Calculate the volume names based on the stack name
|
||||||
|
const stackNameParts = stackResult.data.name.split('_');
|
||||||
|
let volumeNames = [];
|
||||||
|
if (stackNameParts.length >= 3) {
|
||||||
|
const timestamp = stackNameParts.slice(2).join('_');
|
||||||
|
const baseName = `docupulse_${data.port}_${timestamp}`;
|
||||||
|
volumeNames = [
|
||||||
|
`${baseName}_postgres_data`,
|
||||||
|
`${baseName}_uploads`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
stackDetails.innerHTML = `
|
stackDetails.innerHTML = `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -583,9 +665,25 @@ async function startLaunch(data) {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Volume Names</td>
|
||||||
|
<td>
|
||||||
|
<div class="small">
|
||||||
|
${volumeNames.length > 0 ? volumeNames.map(name =>
|
||||||
|
`<code class="text-primary">${name}</code>`
|
||||||
|
).join('<br>') : 'Using default volume names'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
${volumeNames.length > 0 ? `
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
<strong>Volume Naming Convention:</strong> Volumes have been named using the same timestamp as the stack for easy identification and management.
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1249,6 +1347,183 @@ Thank you for choosing DocuPulse!
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to handle instance updates
|
||||||
|
async function startUpdate(data) {
|
||||||
|
console.log('Starting instance update:', data);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update the header to reflect this is an update
|
||||||
|
const headerTitle = document.querySelector('.header h1');
|
||||||
|
const headerDescription = document.querySelector('.header p');
|
||||||
|
if (headerTitle) headerTitle.textContent = 'Updating Instance';
|
||||||
|
if (headerDescription) headerDescription.textContent = 'Updating your DocuPulse instance with the latest version';
|
||||||
|
|
||||||
|
// Initialize launch report for update
|
||||||
|
const launchReport = {
|
||||||
|
type: 'update',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
instanceId: data.instanceId,
|
||||||
|
repository: data.repository,
|
||||||
|
branch: data.branch,
|
||||||
|
steps: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 1: Check Portainer Connection
|
||||||
|
await updateStep(1, 'Checking Portainer Connection', 'Verifying connection to Portainer...');
|
||||||
|
const portainerResult = await checkPortainerConnection();
|
||||||
|
if (!portainerResult.success) {
|
||||||
|
throw new Error(`Portainer connection failed: ${portainerResult.error}`);
|
||||||
|
}
|
||||||
|
launchReport.steps.push({
|
||||||
|
step: 'Portainer Connection',
|
||||||
|
status: 'success',
|
||||||
|
details: portainerResult
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Download Docker Compose
|
||||||
|
await updateStep(2, 'Downloading Docker Compose', 'Fetching docker-compose.yml from repository...');
|
||||||
|
const dockerComposeResult = await downloadDockerCompose(data.repository, data.branch);
|
||||||
|
if (!dockerComposeResult.success) {
|
||||||
|
throw new Error(`Failed to download Docker Compose: ${dockerComposeResult.error}`);
|
||||||
|
}
|
||||||
|
launchReport.steps.push({
|
||||||
|
step: 'Docker Compose Download',
|
||||||
|
status: 'success',
|
||||||
|
details: dockerComposeResult
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Deploy Updated Stack
|
||||||
|
await updateStep(3, 'Deploying Updated Stack', 'Deploying the updated application stack...');
|
||||||
|
|
||||||
|
// Get the existing instance information to extract port and stack details
|
||||||
|
const instanceResponse = await fetch(`/api/instances/${data.instanceId}`);
|
||||||
|
if (!instanceResponse.ok) {
|
||||||
|
throw new Error('Failed to get instance information');
|
||||||
|
}
|
||||||
|
const instanceData = await instanceResponse.json();
|
||||||
|
const port = instanceData.instance.name; // Assuming the instance name is the port
|
||||||
|
|
||||||
|
// Check if we have an existing stack ID
|
||||||
|
if (!instanceData.instance.portainer_stack_id) {
|
||||||
|
throw new Error('No existing stack found for this instance. Cannot perform update.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the existing stack instead of creating a new one
|
||||||
|
const stackResult = await updateStack(dockerComposeResult.content, instanceData.instance.portainer_stack_id, port);
|
||||||
|
if (!stackResult.success) {
|
||||||
|
throw new Error(`Failed to update stack: ${stackResult.error}`);
|
||||||
|
}
|
||||||
|
launchReport.steps.push({
|
||||||
|
step: 'Stack Update',
|
||||||
|
status: 'success',
|
||||||
|
details: stackResult
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 4: Update Instance Data
|
||||||
|
await updateStep(4, 'Updating Instance Data', 'Updating instance information...');
|
||||||
|
const updateData = {
|
||||||
|
name: instanceData.instance.name,
|
||||||
|
port: port,
|
||||||
|
domains: instanceData.instance.main_url ? [instanceData.instance.main_url.replace(/^https?:\/\//, '')] : [],
|
||||||
|
stack_id: instanceData.instance.portainer_stack_id, // Keep the same stack ID
|
||||||
|
stack_name: instanceData.instance.portainer_stack_name, // Keep the same stack name
|
||||||
|
status: stackResult.data.status,
|
||||||
|
repository: data.repository,
|
||||||
|
branch: data.branch,
|
||||||
|
deployed_version: dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown',
|
||||||
|
deployed_branch: data.branch,
|
||||||
|
payment_plan: instanceData.instance.payment_plan || 'Basic'
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveResult = await saveInstanceData(updateData);
|
||||||
|
if (!saveResult.success) {
|
||||||
|
throw new Error(`Failed to update instance data: ${saveResult.error}`);
|
||||||
|
}
|
||||||
|
launchReport.steps.push({
|
||||||
|
step: 'Instance Data Update',
|
||||||
|
status: 'success',
|
||||||
|
details: saveResult
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 5: Health Check
|
||||||
|
await updateStep(5, 'Health Check', 'Verifying updated instance health...');
|
||||||
|
const healthResult = await checkInstanceHealth(instanceData.instance.main_url);
|
||||||
|
if (!healthResult.success) {
|
||||||
|
throw new Error(`Health check failed: ${healthResult.error}`);
|
||||||
|
}
|
||||||
|
launchReport.steps.push({
|
||||||
|
step: 'Health Check',
|
||||||
|
status: 'success',
|
||||||
|
details: healthResult
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update completed successfully
|
||||||
|
await updateStep(6, 'Update Complete', 'Instance has been successfully updated!');
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
const successStep = document.querySelectorAll('.step-item')[5];
|
||||||
|
successStep.classList.remove('active');
|
||||||
|
successStep.classList.add('completed');
|
||||||
|
successStep.querySelector('.step-status').textContent = 'Instance updated successfully!';
|
||||||
|
|
||||||
|
// Add success details
|
||||||
|
const successDetails = document.createElement('div');
|
||||||
|
successDetails.className = 'mt-3';
|
||||||
|
|
||||||
|
// Calculate the volume names based on the stack name
|
||||||
|
const stackNameParts = stackResult.data.name.split('_');
|
||||||
|
let volumeNames = [];
|
||||||
|
if (stackNameParts.length >= 3) {
|
||||||
|
const timestamp = stackNameParts.slice(2).join('_');
|
||||||
|
const baseName = `docupulse_${port}_${timestamp}`;
|
||||||
|
volumeNames = [
|
||||||
|
`${baseName}_postgres_data`,
|
||||||
|
`${baseName}_uploads`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
successDetails.innerHTML = `
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<h6><i class="fas fa-check-circle me-2"></i>Update Completed Successfully!</h6>
|
||||||
|
<p class="mb-2">Your instance has been updated with the latest version from the repository. All existing data and volumes have been preserved.</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Repository:</strong> ${data.repository}<br>
|
||||||
|
<strong>Branch:</strong> ${data.branch}<br>
|
||||||
|
<strong>New Version:</strong> ${dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown'}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Stack Name:</strong> ${stackResult.data.name}<br>
|
||||||
|
<strong>Instance URL:</strong> <a href="${instanceData.instance.main_url}" target="_blank">${instanceData.instance.main_url}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
<strong>Data Preservation:</strong> All existing data, volumes, and configurations have been preserved during this update.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
successStep.querySelector('.step-content').appendChild(successDetails);
|
||||||
|
|
||||||
|
// Add button to return to instances page
|
||||||
|
const returnButton = document.createElement('button');
|
||||||
|
returnButton.className = 'btn btn-primary mt-3';
|
||||||
|
returnButton.innerHTML = '<i class="fas fa-arrow-left me-2"></i>Return to Instances';
|
||||||
|
returnButton.onclick = () => window.location.href = '/instances';
|
||||||
|
successStep.querySelector('.step-content').appendChild(returnButton);
|
||||||
|
|
||||||
|
// Store the update report
|
||||||
|
sessionStorage.setItem('instanceUpdateReport', JSON.stringify(launchReport));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Update failed:', error);
|
||||||
|
await updateStep(6, 'Update Failed', `Error: ${error.message}`);
|
||||||
|
showError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function checkDNSRecords(domains) {
|
async function checkDNSRecords(domains) {
|
||||||
const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes
|
const maxRetries = 30; // 30 retries * 10 seconds = 5 minutes
|
||||||
const baseDelay = 10000; // 10 seconds base delay
|
const baseDelay = 10000; // 10 seconds base delay
|
||||||
@@ -2645,7 +2920,34 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update volume names in docker-compose content to match stack naming convention
|
||||||
|
let modifiedDockerComposeContent = dockerComposeContent;
|
||||||
|
|
||||||
|
// Extract timestamp from stack name (format: docupulse_PORT_TIMESTAMP)
|
||||||
|
const stackNameParts = stackName.split('_');
|
||||||
|
if (stackNameParts.length >= 3) {
|
||||||
|
const timestamp = stackNameParts.slice(2).join('_'); // Get everything after port
|
||||||
|
const baseName = `docupulse_${port}_${timestamp}`;
|
||||||
|
|
||||||
|
// Replace volume names to match stack naming convention
|
||||||
|
modifiedDockerComposeContent = modifiedDockerComposeContent.replace(
|
||||||
|
/name: docupulse_\$\{PORT:-10335\}_postgres_data/g,
|
||||||
|
`name: ${baseName}_postgres_data`
|
||||||
|
);
|
||||||
|
modifiedDockerComposeContent = modifiedDockerComposeContent.replace(
|
||||||
|
/name: docupulse_\$\{PORT:-10335\}_uploads/g,
|
||||||
|
`name: ${baseName}_uploads`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Updated volume names to match stack naming convention: ${baseName}`);
|
||||||
|
}
|
||||||
|
|
||||||
// First, attempt to deploy the stack
|
// First, attempt to deploy the stack
|
||||||
|
console.log('Making stack deployment request to /api/admin/deploy-stack');
|
||||||
|
console.log('Stack name:', stackName);
|
||||||
|
console.log('Port:', port);
|
||||||
|
console.log('Modified docker-compose content length:', modifiedDockerComposeContent.length);
|
||||||
|
|
||||||
const response = await fetch('/api/admin/deploy-stack', {
|
const response = await fetch('/api/admin/deploy-stack', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2654,7 +2956,7 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: stackName,
|
name: stackName,
|
||||||
StackFileContent: dockerComposeContent,
|
StackFileContent: modifiedDockerComposeContent,
|
||||||
Env: [
|
Env: [
|
||||||
{
|
{
|
||||||
name: 'PORT',
|
name: 'PORT',
|
||||||
@@ -2713,13 +3015,17 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response ok:', response.ok);
|
||||||
|
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
|
||||||
|
|
||||||
// Handle 504 Gateway Timeout as successful initiation
|
// Handle 504 Gateway Timeout as successful initiation
|
||||||
if (response.status === 504) {
|
if (response.status === 504) {
|
||||||
console.log('Received 504 Gateway Timeout - stack creation may still be in progress');
|
console.log('Received 504 Gateway Timeout - stack creation may still be in progress');
|
||||||
|
|
||||||
// Update progress to show that we're now polling
|
// Update progress to show that we're now polling
|
||||||
const progressBar = document.getElementById('stackProgress');
|
const progressBar = document.getElementById('launchProgress');
|
||||||
const progressText = document.getElementById('stackProgressText');
|
const progressText = document.getElementById('stepDescription');
|
||||||
if (progressBar && progressText) {
|
if (progressBar && progressText) {
|
||||||
progressBar.style.width = '25%';
|
progressBar.style.width = '25%';
|
||||||
progressBar.textContent = '25%';
|
progressBar.textContent = '25%';
|
||||||
@@ -2733,8 +3039,45 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
let errorMessage = 'Failed to deploy stack';
|
||||||
throw new Error(error.error || 'Failed to deploy stack');
|
console.log('Response not ok, status:', response.status);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMessage = errorData.error || errorMessage;
|
||||||
|
console.log('Parsed error data:', errorData);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log('Failed to parse JSON error, trying text:', parseError);
|
||||||
|
// If JSON parsing fails, try to get text content
|
||||||
|
try {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.log('Error text content:', errorText);
|
||||||
|
if (errorText.includes('504 Gateway Time-out') || errorText.includes('504 Gateway Timeout')) {
|
||||||
|
console.log('Received 504 Gateway Timeout - stack creation may still be in progress');
|
||||||
|
|
||||||
|
// Update progress to show that we're now polling
|
||||||
|
const progressBar = document.getElementById('launchProgress');
|
||||||
|
const progressText = document.getElementById('stepDescription');
|
||||||
|
if (progressBar && progressText) {
|
||||||
|
progressBar.style.width = '25%';
|
||||||
|
progressBar.textContent = '25%';
|
||||||
|
progressText.textContent = 'Stack creation initiated (timed out, but continuing to monitor)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling immediately since the stack creation was initiated
|
||||||
|
console.log('Starting to poll for stack status after 504 timeout...');
|
||||||
|
const pollResult = await pollStackStatus(stackName, 15 * 60 * 1000); // 15 minutes max
|
||||||
|
return pollResult;
|
||||||
|
} else {
|
||||||
|
errorMessage = `HTTP ${response.status}: ${errorText}`;
|
||||||
|
}
|
||||||
|
} catch (textError) {
|
||||||
|
console.log('Failed to get error text:', textError);
|
||||||
|
errorMessage = `HTTP ${response.status}: Failed to parse response`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Throwing error:', errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -2755,6 +3098,30 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deploying stack:', error);
|
console.error('Error deploying stack:', error);
|
||||||
|
|
||||||
|
// Check if this is a 504 timeout error that should be handled as a success
|
||||||
|
if (error.message && (
|
||||||
|
error.message.includes('504 Gateway Time-out') ||
|
||||||
|
error.message.includes('504 Gateway Timeout') ||
|
||||||
|
error.message.includes('timed out')
|
||||||
|
)) {
|
||||||
|
console.log('Detected 504 timeout in catch block - treating as successful initiation');
|
||||||
|
|
||||||
|
// Update progress to show that we're now polling
|
||||||
|
const progressBar = document.getElementById('launchProgress');
|
||||||
|
const progressText = document.getElementById('stepDescription');
|
||||||
|
if (progressBar && progressText) {
|
||||||
|
progressBar.style.width = '25%';
|
||||||
|
progressBar.textContent = '25%';
|
||||||
|
progressText.textContent = 'Stack creation initiated (timed out, but continuing to monitor)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling immediately since the stack creation was initiated
|
||||||
|
console.log('Starting to poll for stack status after 504 timeout from catch block...');
|
||||||
|
const pollResult = await pollStackStatus(stackName, 15 * 60 * 1000); // 15 minutes max
|
||||||
|
return pollResult;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message
|
error: error.message
|
||||||
@@ -2763,34 +3130,34 @@ async function deployStack(dockerComposeContent, stackName, port) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function to poll stack status
|
// Function to poll stack status
|
||||||
async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
async function pollStackStatus(stackIdentifier, maxWaitTime = 15 * 60 * 1000) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const pollInterval = 5000; // 5 seconds
|
const pollInterval = 5000; // 5 seconds
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let lastKnownStatus = 'unknown';
|
let lastKnownStatus = 'unknown';
|
||||||
|
|
||||||
// Validate stack name
|
// Validate stack identifier (can be name or ID)
|
||||||
if (!stackName || typeof stackName !== 'string') {
|
if (!stackIdentifier || typeof stackIdentifier !== 'string') {
|
||||||
console.error('Invalid stack name provided to pollStackStatus:', stackName);
|
console.error('Invalid stack identifier provided to pollStackStatus:', stackIdentifier);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Invalid stack name: ${stackName}`,
|
error: `Invalid stack identifier: ${stackIdentifier}`,
|
||||||
data: {
|
data: {
|
||||||
name: stackName,
|
name: stackIdentifier,
|
||||||
status: 'error'
|
status: 'error'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Starting to poll stack status for: ${stackName} (max wait: ${maxWaitTime / 1000}s)`);
|
console.log(`Starting to poll stack status for: ${stackIdentifier} (max wait: ${maxWaitTime / 1000}s)`);
|
||||||
|
|
||||||
// Update progress indicator
|
// Update progress indicator
|
||||||
const progressBar = document.getElementById('stackProgress');
|
const progressBar = document.getElementById('launchProgress');
|
||||||
const progressText = document.getElementById('stackProgressText');
|
const progressText = document.getElementById('stepDescription');
|
||||||
|
|
||||||
while (Date.now() - startTime < maxWaitTime) {
|
while (Date.now() - startTime < maxWaitTime) {
|
||||||
attempts++;
|
attempts++;
|
||||||
console.log(`Polling attempt ${attempts} for stack: ${stackName}`);
|
console.log(`Polling attempt ${attempts} for stack: ${stackIdentifier}`);
|
||||||
|
|
||||||
// Update progress - start at 25% if we came from a 504 timeout, otherwise start at 0%
|
// Update progress - start at 25% if we came from a 504 timeout, otherwise start at 0%
|
||||||
const elapsed = Date.now() - startTime;
|
const elapsed = Date.now() - startTime;
|
||||||
@@ -2802,9 +3169,12 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestBody = {
|
// Determine if this is a stack ID (numeric) or stack name
|
||||||
stack_name: stackName
|
const isStackId = /^\d+$/.test(stackIdentifier);
|
||||||
};
|
const requestBody = isStackId ?
|
||||||
|
{ stack_id: stackIdentifier } :
|
||||||
|
{ stack_name: stackIdentifier };
|
||||||
|
|
||||||
console.log(`Sending stack status check request:`, requestBody);
|
console.log(`Sending stack status check request:`, requestBody);
|
||||||
|
|
||||||
const response = await fetch('/api/admin/check-stack-status', {
|
const response = await fetch('/api/admin/check-stack-status', {
|
||||||
@@ -2822,7 +3192,7 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
|||||||
console.log(`Stack status check result:`, result);
|
console.log(`Stack status check result:`, result);
|
||||||
|
|
||||||
if (result.data && result.data.status === 'active') {
|
if (result.data && result.data.status === 'active') {
|
||||||
console.log(`Stack ${stackName} is now active!`);
|
console.log(`Stack ${stackIdentifier} is now active!`);
|
||||||
// Update progress to 100%
|
// Update progress to 100%
|
||||||
if (progressBar && progressText) {
|
if (progressBar && progressText) {
|
||||||
progressBar.style.width = '100%';
|
progressBar.style.width = '100%';
|
||||||
@@ -2834,25 +3204,25 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
name: stackName,
|
name: result.data.name || stackIdentifier,
|
||||||
id: result.data.stack_id,
|
id: result.data.stack_id || stackIdentifier,
|
||||||
status: 'active'
|
status: 'active'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else if (result.data && result.data.status === 'partial') {
|
} else if (result.data && result.data.status === 'partial') {
|
||||||
console.log(`Stack ${stackName} is partially running, continuing to poll...`);
|
console.log(`Stack ${stackIdentifier} is partially running, continuing to poll...`);
|
||||||
lastKnownStatus = 'partial';
|
lastKnownStatus = 'partial';
|
||||||
if (progressText) {
|
if (progressText) {
|
||||||
progressText.textContent = `Stack is partially running (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
progressText.textContent = `Stack is partially running (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||||
}
|
}
|
||||||
} else if (result.data && result.data.status === 'inactive') {
|
} else if (result.data && result.data.status === 'inactive') {
|
||||||
console.log(`Stack ${stackName} is inactive, continuing to poll...`);
|
console.log(`Stack ${stackIdentifier} is inactive, continuing to poll...`);
|
||||||
lastKnownStatus = 'inactive';
|
lastKnownStatus = 'inactive';
|
||||||
if (progressText) {
|
if (progressText) {
|
||||||
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
progressText.textContent = `Stack is starting up (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||||
}
|
}
|
||||||
} else if (result.data && result.data.status === 'starting') {
|
} else if (result.data && result.data.status === 'starting') {
|
||||||
console.log(`Stack ${stackName} exists and is starting up - continuing to next step`);
|
console.log(`Stack ${stackIdentifier} exists and is starting up - continuing to next step`);
|
||||||
// Stack exists, we can continue - no need to wait for all services
|
// Stack exists, we can continue - no need to wait for all services
|
||||||
if (progressBar && progressText) {
|
if (progressBar && progressText) {
|
||||||
progressBar.style.width = '100%';
|
progressBar.style.width = '100%';
|
||||||
@@ -2864,20 +3234,20 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
name: stackName,
|
name: result.data.name || stackIdentifier,
|
||||||
id: result.data.stack_id,
|
id: result.data.stack_id || stackIdentifier,
|
||||||
status: 'starting'
|
status: 'starting'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.log(`Stack ${stackName} status unknown, continuing to poll...`);
|
console.log(`Stack ${stackIdentifier} status unknown, continuing to poll...`);
|
||||||
lastKnownStatus = 'unknown';
|
lastKnownStatus = 'unknown';
|
||||||
if (progressText) {
|
if (progressText) {
|
||||||
progressText.textContent = `Checking stack status (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
progressText.textContent = `Checking stack status (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (response.status === 404) {
|
} else if (response.status === 404) {
|
||||||
console.log(`Stack ${stackName} not found yet, continuing to poll...`);
|
console.log(`Stack ${stackIdentifier} not found yet, continuing to poll...`);
|
||||||
lastKnownStatus = 'not_found';
|
lastKnownStatus = 'not_found';
|
||||||
if (progressText) {
|
if (progressText) {
|
||||||
progressText.textContent = `Stack not found yet (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
progressText.textContent = `Stack not found yet (${attempts} attempts, ${Math.round(elapsed / 1000)}s elapsed)...`;
|
||||||
@@ -2914,7 +3284,7 @@ async function pollStackStatus(stackName, maxWaitTime = 15 * 60 * 1000) {
|
|||||||
success: false,
|
success: false,
|
||||||
error: `Stack deployment timed out after ${Math.round(maxWaitTime / 1000)} seconds${statusMessage}. The stack may still be deploying in the background.`,
|
error: `Stack deployment timed out after ${Math.round(maxWaitTime / 1000)} seconds${statusMessage}. The stack may still be deploying in the background.`,
|
||||||
data: {
|
data: {
|
||||||
name: stackName,
|
name: stackIdentifier,
|
||||||
status: lastKnownStatus
|
status: lastKnownStatus
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2931,3 +3301,162 @@ function generateStackName(port) {
|
|||||||
now.getSeconds().toString().padStart(2, '0');
|
now.getSeconds().toString().padStart(2, '0');
|
||||||
return `docupulse_${port}_${timestamp}`;
|
return `docupulse_${port}_${timestamp}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add new function to update existing stack
|
||||||
|
async function updateStack(dockerComposeContent, stackId, port) {
|
||||||
|
try {
|
||||||
|
console.log('Updating existing stack:', stackId);
|
||||||
|
console.log('Port:', port);
|
||||||
|
console.log('Modified docker-compose content length:', dockerComposeContent.length);
|
||||||
|
|
||||||
|
const response = await fetch('/api/admin/update-stack', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
stack_id: stackId,
|
||||||
|
StackFileContent: dockerComposeContent,
|
||||||
|
Env: [
|
||||||
|
{
|
||||||
|
name: 'PORT',
|
||||||
|
value: port.toString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ISMASTER',
|
||||||
|
value: 'false'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'APP_VERSION',
|
||||||
|
value: window.currentDeploymentVersion || 'unknown'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GIT_COMMIT',
|
||||||
|
value: window.currentDeploymentCommit || 'unknown'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GIT_BRANCH',
|
||||||
|
value: window.currentDeploymentBranch || 'unknown'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DEPLOYED_AT',
|
||||||
|
value: new Date().toISOString()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Update response status:', response.status);
|
||||||
|
console.log('Update response ok:', response.ok);
|
||||||
|
console.log('Update response headers:', Object.fromEntries(response.headers.entries()));
|
||||||
|
|
||||||
|
// Handle 504 Gateway Timeout as successful initiation
|
||||||
|
if (response.status === 504) {
|
||||||
|
console.log('Received 504 Gateway Timeout - stack update may still be in progress');
|
||||||
|
|
||||||
|
// Update progress to show that we're now polling
|
||||||
|
const progressBar = document.getElementById('launchProgress');
|
||||||
|
const progressText = document.getElementById('stepDescription');
|
||||||
|
if (progressBar && progressText) {
|
||||||
|
progressBar.style.width = '25%';
|
||||||
|
progressBar.textContent = '25%';
|
||||||
|
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling immediately since the stack update was initiated
|
||||||
|
console.log('Starting to poll for stack status after 504 timeout...');
|
||||||
|
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
|
||||||
|
return pollResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = 'Failed to update stack';
|
||||||
|
console.log('Response not ok, status:', response.status);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
errorMessage = errorData.error || errorMessage;
|
||||||
|
console.log('Parsed error data:', errorData);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.log('Failed to parse JSON error, trying text:', parseError);
|
||||||
|
// If JSON parsing fails, try to get text content
|
||||||
|
try {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.log('Error text content:', errorText);
|
||||||
|
if (errorText.includes('504 Gateway Time-out') || errorText.includes('504 Gateway Timeout')) {
|
||||||
|
console.log('Received 504 Gateway Timeout - stack update may still be in progress');
|
||||||
|
|
||||||
|
// Update progress to show that we're now polling
|
||||||
|
const progressBar = document.getElementById('launchProgress');
|
||||||
|
const progressText = document.getElementById('stepDescription');
|
||||||
|
if (progressBar && progressText) {
|
||||||
|
progressBar.style.width = '25%';
|
||||||
|
progressBar.textContent = '25%';
|
||||||
|
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling immediately since the stack update was initiated
|
||||||
|
console.log('Starting to poll for stack status after 504 timeout...');
|
||||||
|
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
|
||||||
|
return pollResult;
|
||||||
|
} else {
|
||||||
|
errorMessage = `HTTP ${response.status}: ${errorText}`;
|
||||||
|
}
|
||||||
|
} catch (textError) {
|
||||||
|
console.log('Failed to get error text:', textError);
|
||||||
|
errorMessage = `HTTP ${response.status}: Failed to parse response`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Throwing error:', errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('Stack update initiated:', result);
|
||||||
|
|
||||||
|
// If stack is being updated, poll for status
|
||||||
|
if (result.data.status === 'updating') {
|
||||||
|
console.log('Stack is being updated, polling for status...');
|
||||||
|
const pollResult = await pollStackStatus(stackId, 10 * 60 * 1000); // 10 minutes max
|
||||||
|
return pollResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success result with response data
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result.data
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating stack:', error);
|
||||||
|
|
||||||
|
// Check if this is a 504 timeout error that should be handled as a success
|
||||||
|
if (error.message && (
|
||||||
|
error.message.includes('504 Gateway Time-out') ||
|
||||||
|
error.message.includes('504 Gateway Timeout') ||
|
||||||
|
error.message.includes('timed out')
|
||||||
|
)) {
|
||||||
|
console.log('Detected 504 timeout in catch block - treating as successful initiation');
|
||||||
|
|
||||||
|
// Update progress to show that we're now polling
|
||||||
|
const progressBar = document.getElementById('launchProgress');
|
||||||
|
const progressText = document.getElementById('stepDescription');
|
||||||
|
if (progressBar && progressText) {
|
||||||
|
progressBar.style.width = '25%';
|
||||||
|
progressBar.textContent = '25%';
|
||||||
|
progressText.textContent = 'Stack update initiated (timed out, but continuing to monitor)...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling immediately since the stack update was initiated
|
||||||
|
console.log('Starting to poll for stack status after 504 timeout from catch block...');
|
||||||
|
const pollResult = await pollStackStatus(stackId, 15 * 60 * 1000); // 15 minutes max
|
||||||
|
return pollResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -226,6 +226,9 @@
|
|||||||
<button class="btn btn-sm btn-outline-info" onclick="window.location.href='/instances/{{ instance.id }}/detail'">
|
<button class="btn btn-sm btn-outline-info" onclick="window.location.href='/instances/{{ instance.id }}/detail'">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-warning" onclick="showUpdateInstanceModal({{ instance.id }}, '{{ instance.portainer_stack_name }}', '{{ instance.main_url }}')" title="Update Instance">
|
||||||
|
<i class="fas fa-arrow-up"></i>
|
||||||
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-danger" type="button" onclick="showDeleteInstanceModal({{ instance.id }})">
|
<button class="btn btn-sm btn-outline-danger" type="button" onclick="showDeleteInstanceModal({{ instance.id }})">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -719,6 +722,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Update Instance Modal -->
|
||||||
|
<div class="modal fade" id="updateInstanceModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Update Instance</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="updateInstanceForm">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
|
<input type="hidden" id="update_instance_id">
|
||||||
|
<input type="hidden" id="update_stack_name">
|
||||||
|
<input type="hidden" id="update_instance_url">
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<i class="fas fa-arrow-up fa-3x mb-3" style="color: var(--primary-color);"></i>
|
||||||
|
<h4>Update Instance</h4>
|
||||||
|
<p class="text-muted">Update your instance with the latest version from the repository.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Repository</label>
|
||||||
|
<select class="form-select" id="updateRepoSelect">
|
||||||
|
<option value="">Loading repositories...</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">Select the repository containing your application code</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Branch</label>
|
||||||
|
<select class="form-select" id="updateBranchSelect" disabled>
|
||||||
|
<option value="">Select a repository first</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">Select the branch to deploy</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6><i class="fas fa-info-circle me-2"></i>Update Process:</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>Download the latest code from the selected repository and branch</li>
|
||||||
|
<li>Build a new Docker image with the updated code</li>
|
||||||
|
<li>Deploy the updated stack to replace the current instance</li>
|
||||||
|
<li>Preserve all existing data and configuration</li>
|
||||||
|
<li>Maintain the same port and domain configuration</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>Important Notes:</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li>The instance will be temporarily unavailable during the update process</li>
|
||||||
|
<li>All existing data, rooms, and conversations will be preserved</li>
|
||||||
|
<li>The update process may take several minutes to complete</li>
|
||||||
|
<li>You will be redirected to a progress page to monitor the update</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-warning" onclick="startInstanceUpdate()">
|
||||||
|
<i class="fas fa-arrow-up me-1"></i>Start Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ header(
|
{{ header(
|
||||||
title="Launching Instance",
|
title=is_update and "Updating Instance" or "Launching Instance",
|
||||||
description="Setting up your new DocuPulse instance",
|
description=is_update and "Updating your DocuPulse instance with the latest version" or "Setting up your new DocuPulse instance",
|
||||||
icon="fa-rocket"
|
icon="fa-arrow-up" if is_update else "fa-rocket"
|
||||||
) }}
|
) }}
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@@ -78,6 +78,12 @@
|
|||||||
|
|
||||||
// Pass CSRF token to JavaScript
|
// Pass CSRF token to JavaScript
|
||||||
window.csrfToken = '{{ csrf_token }}';
|
window.csrfToken = '{{ csrf_token }}';
|
||||||
|
|
||||||
|
// Pass update parameters if this is an update operation
|
||||||
|
window.isUpdate = {{ 'true' if is_update else 'false' }};
|
||||||
|
window.updateInstanceId = '{{ instance_id or "" }}';
|
||||||
|
window.updateRepoId = '{{ repo_id or "" }}';
|
||||||
|
window.updateBranch = '{{ branch or "" }}';
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/launch_progress.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/launch_progress.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user