delete functionality on instances page

This commit is contained in:
2025-06-25 11:58:37 +02:00
parent e519dc3a8b
commit 0466b11c71
5 changed files with 388 additions and 15 deletions

View File

@@ -19,6 +19,7 @@ import smtplib
import requests
from functools import wraps
import socket
from urllib.parse import urlparse
# Set up logging to show in console
logging.basicConfig(
@@ -491,13 +492,197 @@ def init_routes(main_bp):
return jsonify({'error': 'Unauthorized'}), 403
instance = Instance.query.get_or_404(instance_id)
# Get Portainer settings
portainer_settings = KeyValueSettings.get_value('portainer_settings')
if not portainer_settings:
current_app.logger.warning(f"Portainer settings not configured, proceeding with database deletion only for instance {instance.name}")
# Continue with database deletion even if Portainer is not configured
try:
db.session.delete(instance)
db.session.commit()
current_app.logger.info(f"Successfully deleted instance from database: {instance.name}")
return jsonify({'message': 'Instance deleted from database (Portainer not configured)'})
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error deleting instance {instance.name} from database: {str(e)}")
return jsonify({'error': f'Failed to delete instance: {str(e)}'}), 500
try:
# First, delete the Portainer stack and its volumes if stack information exists
if instance.portainer_stack_id and instance.portainer_stack_name:
current_app.logger.info(f"Deleting Portainer stack: {instance.portainer_stack_name} (ID: {instance.portainer_stack_id})")
# Get Portainer endpoint ID (assuming it's the first endpoint)
try:
endpoint_response = requests.get(
f"{portainer_settings['url'].rstrip('/')}/api/endpoints",
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
timeout=30
)
if not endpoint_response.ok:
current_app.logger.error(f"Failed to get Portainer endpoints: {endpoint_response.text}")
return jsonify({'error': 'Failed to get Portainer endpoints'}), 500
endpoints = endpoint_response.json()
if not endpoints:
current_app.logger.error("No Portainer endpoints found")
return jsonify({'error': 'No Portainer endpoints found'}), 400
endpoint_id = endpoints[0]['Id']
# Delete the stack (this will also remove associated volumes)
delete_url = f"{portainer_settings['url'].rstrip('/')}/api/stacks/{instance.portainer_stack_id}"
delete_response = requests.delete(
delete_url,
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
params={'endpointId': endpoint_id},
timeout=60 # Give more time for stack deletion
)
if delete_response.ok:
current_app.logger.info(f"Successfully deleted Portainer stack: {instance.portainer_stack_name}")
else:
current_app.logger.warning(f"Failed to delete Portainer stack: {delete_response.status_code} - {delete_response.text}")
# Continue with database deletion even if Portainer deletion fails
# Also try to delete any orphaned volumes associated with this stack
try:
volumes_url = f"{portainer_settings['url'].rstrip('/')}/api/endpoints/{endpoint_id}/docker/volumes"
volumes_response = requests.get(
volumes_url,
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
timeout=30
)
if volumes_response.ok:
volumes = volumes_response.json().get('Volumes', [])
stack_volumes = [vol for vol in volumes if vol.get('Labels', {}).get('com.docker.stack.namespace') == instance.portainer_stack_name]
for volume in stack_volumes:
volume_name = volume.get('Name')
if volume_name:
delete_volume_url = f"{volumes_url}/{volume_name}"
volume_delete_response = requests.delete(
delete_volume_url,
headers={
'X-API-Key': portainer_settings['api_key'],
'Accept': 'application/json'
},
timeout=30
)
if volume_delete_response.ok:
current_app.logger.info(f"Successfully deleted volume: {volume_name}")
else:
current_app.logger.warning(f"Failed to delete volume {volume_name}: {volume_delete_response.status_code}")
else:
current_app.logger.warning(f"Failed to get volumes list: {volumes_response.status_code}")
except Exception as volume_error:
current_app.logger.warning(f"Error cleaning up volumes: {str(volume_error)}")
except requests.exceptions.RequestException as req_error:
current_app.logger.error(f"Network error during Portainer operations: {str(req_error)}")
# Continue with database deletion even if Portainer operations fail
else:
current_app.logger.info(f"No Portainer stack information found for instance {instance.name}, proceeding with database deletion only")
# Clean up NGINX proxy host if NGINX settings are configured
nginx_settings = KeyValueSettings.get_value('nginx_settings')
if nginx_settings and instance.main_url:
current_app.logger.info(f"Cleaning up NGINX proxy host for instance {instance.name}")
try:
# Extract domain from main_url
parsed_url = urlparse(instance.main_url)
domain = parsed_url.netloc
if domain:
# Get NGINX JWT token
token_response = requests.post(
f"{nginx_settings['url'].rstrip('/')}/api/tokens",
json={
'identity': nginx_settings['username'],
'secret': nginx_settings['password']
},
headers={'Content-Type': 'application/json'},
timeout=30
)
if token_response.ok:
token_data = token_response.json()
token = token_data.get('token')
if token:
# Get all proxy hosts to find the one matching this domain
proxy_hosts_response = requests.get(
f"{nginx_settings['url'].rstrip('/')}/api/nginx/proxy-hosts",
headers={
'Authorization': f'Bearer {token}',
'Accept': 'application/json'
},
timeout=30
)
if proxy_hosts_response.ok:
proxy_hosts = proxy_hosts_response.json()
# Find proxy host with matching domain
matching_proxy = None
for proxy_host in proxy_hosts:
if proxy_host.get('domain_names') and domain in proxy_host['domain_names']:
matching_proxy = proxy_host
break
if matching_proxy:
# Delete the proxy host
delete_response = requests.delete(
f"{nginx_settings['url'].rstrip('/')}/api/nginx/proxy-hosts/{matching_proxy['id']}",
headers={
'Authorization': f'Bearer {token}',
'Accept': 'application/json'
},
timeout=30
)
if delete_response.ok:
current_app.logger.info(f"Successfully deleted NGINX proxy host for domain: {domain}")
else:
current_app.logger.warning(f"Failed to delete NGINX proxy host: {delete_response.status_code} - {delete_response.text}")
else:
current_app.logger.info(f"No NGINX proxy host found for domain: {domain}")
else:
current_app.logger.warning(f"Failed to get NGINX proxy hosts: {proxy_hosts_response.status_code}")
else:
current_app.logger.warning("No NGINX token received")
else:
current_app.logger.warning(f"Failed to authenticate with NGINX: {token_response.status_code}")
except Exception as nginx_error:
current_app.logger.warning(f"Error cleaning up NGINX proxy host: {str(nginx_error)}")
# Continue with database deletion even if NGINX cleanup fails
else:
current_app.logger.info(f"No NGINX settings configured or no main_url for instance {instance.name}, skipping NGINX cleanup")
# Now delete the instance from the database
db.session.delete(instance)
db.session.commit()
return jsonify({'message': 'Instance deleted successfully'})
current_app.logger.info(f"Successfully deleted instance: {instance.name}")
return jsonify({'message': 'Instance and all associated resources (Portainer stack, volumes, NGINX proxy host) deleted successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 400
current_app.logger.error(f"Error deleting instance {instance.name}: {str(e)}")
return jsonify({'error': f'Failed to delete instance: {str(e)}'}), 500
@main_bp.route('/instances/<int:instance_id>/status')
@login_required