Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b598f2966 | |||
| 77032062a1 | |||
| 81675af837 | |||
| 0a2cddf122 |
Binary file not shown.
@@ -929,8 +929,8 @@ def deploy_stack():
|
||||
def check_stack_status():
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data or 'stack_name' not in data:
|
||||
return jsonify({'error': 'Missing stack_name field'}), 400
|
||||
if not data or ('stack_name' not in data and 'stack_id' not in data):
|
||||
return jsonify({'error': 'Missing stack_name or stack_id field'}), 400
|
||||
|
||||
# Get Portainer settings
|
||||
portainer_settings = KeyValueSettings.get_value('portainer_settings')
|
||||
@@ -956,35 +956,54 @@ def check_stack_status():
|
||||
|
||||
endpoint_id = endpoints[0]['Id']
|
||||
|
||||
# Get stack information
|
||||
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['stack_name']})},
|
||||
timeout=30
|
||||
)
|
||||
# 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 stacks_response.ok:
|
||||
return jsonify({'error': 'Failed to get stack information'}), 500
|
||||
if not stack_response.ok:
|
||||
return jsonify({'error': f'Stack with ID {data["stack_id"]} not found'}), 404
|
||||
|
||||
stacks = stacks_response.json()
|
||||
target_stack = None
|
||||
|
||||
for stack in stacks:
|
||||
if stack['Name'] == data['stack_name']:
|
||||
target_stack = stack
|
||||
break
|
||||
target_stack = stack_response.json()
|
||||
else:
|
||||
# Get stack by name (existing logic)
|
||||
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['stack_name']})},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not target_stack:
|
||||
return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404
|
||||
if not stacks_response.ok:
|
||||
return jsonify({'error': 'Failed to get stack information'}), 500
|
||||
|
||||
stacks = stacks_response.json()
|
||||
target_stack = None
|
||||
|
||||
for stack in stacks:
|
||||
if stack['Name'] == data['stack_name']:
|
||||
target_stack = stack
|
||||
break
|
||||
|
||||
if not target_stack:
|
||||
return jsonify({'error': f'Stack {data["stack_name"]} not found'}), 404
|
||||
|
||||
# Get stack services to check their status
|
||||
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:
|
||||
services_response = requests.get(
|
||||
@@ -993,7 +1012,7 @@ def check_stack_status():
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'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
|
||||
)
|
||||
|
||||
@@ -1001,46 +1020,40 @@ def check_stack_status():
|
||||
|
||||
if services_response.ok:
|
||||
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 = []
|
||||
|
||||
for service in services:
|
||||
replicas_running = service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0)
|
||||
replicas_actual = service.get('ServiceStatus', {}).get('RunningTasks', 0)
|
||||
|
||||
service_status = {
|
||||
service_statuses.append({
|
||||
'name': service.get('Spec', {}).get('Name', 'Unknown'),
|
||||
'replicas_expected': replicas_running,
|
||||
'replicas_running': replicas_actual,
|
||||
'status': 'running' if replicas_actual >= replicas_running else 'not_running'
|
||||
}
|
||||
|
||||
service_statuses.append(service_status)
|
||||
|
||||
if replicas_actual < replicas_running:
|
||||
all_running = False
|
||||
|
||||
'replicas': service.get('Spec', {}).get('Mode', {}).get('Replicated', {}).get('Replicas', 0),
|
||||
'running_replicas': service.get('ServiceStatus', {}).get('RunningTasks', 0),
|
||||
'desired_replicas': service.get('ServiceStatus', {}).get('DesiredTasks', 0)
|
||||
})
|
||||
|
||||
# Determine overall stack status
|
||||
if all_running and len(services) > 0:
|
||||
status = 'active'
|
||||
elif len(services) > 0:
|
||||
status = 'partial'
|
||||
if not service_statuses:
|
||||
status = 'starting' # No services found yet
|
||||
else:
|
||||
status = 'inactive'
|
||||
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'
|
||||
elif any_running:
|
||||
status = 'partial'
|
||||
else:
|
||||
status = 'inactive'
|
||||
else:
|
||||
# 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
|
||||
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:
|
||||
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:
|
||||
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 = []
|
||||
service_statuses = []
|
||||
@@ -1048,7 +1061,7 @@ def check_stack_status():
|
||||
|
||||
except Exception as e:
|
||||
# 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 = []
|
||||
service_statuses = []
|
||||
status = 'starting' # Stack exists but services not available yet
|
||||
@@ -1056,14 +1069,10 @@ def check_stack_status():
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'stack_name': data['stack_name'],
|
||||
'name': target_stack['Name'],
|
||||
'stack_id': target_stack['Id'],
|
||||
'status': status,
|
||||
'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')
|
||||
'services': service_statuses
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1854,4 +1863,216 @@ def copy_smtp_settings():
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error copying SMTP settings: {str(e)}")
|
||||
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:
|
||||
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 and get its current configuration
|
||||
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']})")
|
||||
|
||||
# Get the current stack file content from Portainer
|
||||
stack_file_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}/file"
|
||||
stack_file_response = requests.get(
|
||||
stack_file_url,
|
||||
headers={
|
||||
'X-API-Key': portainer_settings['api_key'],
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params={'endpointId': endpoint_id},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if not stack_file_response.ok:
|
||||
current_app.logger.error(f"Failed to get stack file: {stack_file_response.text}")
|
||||
return jsonify({'error': f'Failed to get existing stack file: {stack_file_response.text}'}), 500
|
||||
|
||||
stack_file_data = stack_file_response.json()
|
||||
current_stack_file_content = stack_file_data.get('StackFileContent')
|
||||
|
||||
if not current_stack_file_content:
|
||||
current_app.logger.error("No StackFileContent found in existing stack")
|
||||
return jsonify({'error': 'No existing stack file content found'}), 500
|
||||
|
||||
current_app.logger.info("Retrieved existing stack file content")
|
||||
|
||||
# Get existing environment variables from the stack
|
||||
existing_env_vars = stack_file_data.get('Env', [])
|
||||
current_app.logger.info(f"Retrieved {len(existing_env_vars)} existing environment variables")
|
||||
|
||||
# Create a dictionary of existing environment variables for easy lookup
|
||||
existing_env_dict = {env['name']: env['value'] for env in existing_env_vars}
|
||||
current_app.logger.info(f"Existing environment variables: {list(existing_env_dict.keys())}")
|
||||
|
||||
# Get new environment variables from the request
|
||||
new_env_vars = data.get('Env', [])
|
||||
current_app.logger.info(f"New environment variables to update: {[env['name'] for env in new_env_vars]}")
|
||||
|
||||
# Merge existing and new environment variables
|
||||
# Start with existing variables
|
||||
merged_env_vars = existing_env_vars.copy()
|
||||
|
||||
# Update with new variables (this will overwrite existing ones with the same name)
|
||||
for new_env in new_env_vars:
|
||||
# Find if this environment variable already exists
|
||||
existing_index = None
|
||||
for i, existing_env in enumerate(merged_env_vars):
|
||||
if existing_env['name'] == new_env['name']:
|
||||
existing_index = i
|
||||
break
|
||||
|
||||
if existing_index is not None:
|
||||
# Update existing variable
|
||||
merged_env_vars[existing_index]['value'] = new_env['value']
|
||||
current_app.logger.info(f"Updated environment variable: {new_env['name']} = {new_env['value']}")
|
||||
else:
|
||||
# Add new variable
|
||||
merged_env_vars.append(new_env)
|
||||
current_app.logger.info(f"Added new environment variable: {new_env['name']} = {new_env['value']}")
|
||||
|
||||
current_app.logger.info(f"Final merged environment variables: {[env['name'] for env in merged_env_vars]}")
|
||||
|
||||
# Update the stack using Portainer's update API
|
||||
update_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{data['stack_id']}"
|
||||
current_app.logger.info(f"Making update request to: {update_url}")
|
||||
|
||||
# Prepare the request body for stack update
|
||||
request_body = {
|
||||
'StackFileContent': current_stack_file_content, # Use existing stack file content
|
||||
'Env': merged_env_vars # Use merged environment variables
|
||||
}
|
||||
|
||||
# If new StackFileContent is provided, use it instead
|
||||
if 'StackFileContent' in data:
|
||||
request_body['StackFileContent'] = data['StackFileContent']
|
||||
current_app.logger.info("Using provided StackFileContent for update")
|
||||
else:
|
||||
current_app.logger.info("Using existing StackFileContent for update")
|
||||
|
||||
# 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)
|
||||
|
||||
@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')
|
||||
@login_required
|
||||
@require_password_change
|
||||
@@ -2139,6 +2165,12 @@ def init_routes(main_bp):
|
||||
flash('This page is only available in master instances.', 'error')
|
||||
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
|
||||
nginx_settings = KeyValueSettings.get_value('nginx_settings')
|
||||
# Get Portainer settings
|
||||
@@ -2149,7 +2181,11 @@ def init_routes(main_bp):
|
||||
return render_template('main/launch_progress.html',
|
||||
nginx_settings=nginx_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'])
|
||||
@login_required
|
||||
|
||||
@@ -4,6 +4,7 @@ let editInstanceModal;
|
||||
let addExistingInstanceModal;
|
||||
let authModal;
|
||||
let launchStepsModal;
|
||||
let updateInstanceModal;
|
||||
let currentStep = 1;
|
||||
|
||||
// Update the total number of steps
|
||||
@@ -15,6 +16,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
addExistingInstanceModal = new bootstrap.Modal(document.getElementById('addExistingInstanceModal'));
|
||||
authModal = new bootstrap.Modal(document.getElementById('authModal'));
|
||||
launchStepsModal = new bootstrap.Modal(document.getElementById('launchStepsModal'));
|
||||
updateInstanceModal = new bootstrap.Modal(document.getElementById('updateInstanceModal'));
|
||||
|
||||
// Initialize tooltips
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
@@ -1774,4 +1776,168 @@ async function confirmDeleteInstance() {
|
||||
confirmDeleteBtn.className = 'btn btn-danger';
|
||||
}, 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -226,6 +226,9 @@
|
||||
<button class="btn btn-sm btn-outline-info" onclick="window.location.href='/instances/{{ instance.id }}/detail'">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</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 }})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -719,6 +722,76 @@
|
||||
</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 %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
{% block content %}
|
||||
{{ header(
|
||||
title="Launching Instance",
|
||||
description="Setting up your new DocuPulse instance",
|
||||
icon="fa-rocket"
|
||||
title=is_update and "Updating Instance" or "Launching Instance",
|
||||
description=is_update and "Updating your DocuPulse instance with the latest version" or "Setting up your new DocuPulse instance",
|
||||
icon="fa-arrow-up" if is_update else "fa-rocket"
|
||||
) }}
|
||||
|
||||
<div class="container-fluid">
|
||||
@@ -78,6 +78,12 @@
|
||||
|
||||
// Pass CSRF token to JavaScript
|
||||
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 src="{{ url_for('static', filename='js/launch_progress.js') }}"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user