From 0da5d9305def9dd05ff73f2772a248922e7b6c7f Mon Sep 17 00:00:00 2001 From: Kobe Date: Mon, 23 Jun 2025 09:30:04 +0200 Subject: [PATCH] version --- __pycache__/models.cpython-313.pyc | Bin 39018 -> 39440 bytes .../versions/a1fd98e6630d_merge_heads.py | 24 +++ .../add_password_reset_tokens_table.py | 36 ++-- ...dd_version_tracking_fields_to_instances.py | 68 ++++++ models.py | 5 + routes/__pycache__/main.cpython-313.pyc | Bin 100875 -> 104187 bytes routes/launch_api.py | 194 ++++++++++++++++-- routes/main.py | 80 +++++++- static/js/launch_progress.js | 11 +- templates/main/instances.html | 60 ++++++ version.txt | 6 + 11 files changed, 444 insertions(+), 40 deletions(-) create mode 100644 migrations/versions/a1fd98e6630d_merge_heads.py create mode 100644 migrations/versions/c94c2b2b9f2e_add_version_tracking_fields_to_instances.py create mode 100644 version.txt diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 3638571fd4e9fcc4cd85ea6ae10d54edbe7a5db1..3cc391e0b2a6d1aa7bd165b21f6edf48049a543f 100644 GIT binary patch delta 456 zcmaF0foZ}PCce+Syj%=G@P;umV{P0Dwq zQV0=7w#jjFlKet&lZ7pri)7H{IpOjk)v|D2u!to~k!-N26ho0*uvn3Ny11sqWQU2# z%ti5xlNV0h#V9>FW|HRQ3SIrlTPC&m+!9DhEy&5QOihU|OD!tS%+I^U4;M))O3X{n zxW$)~Sdv;?0#i`r1N81KA*k~Bvwk&_~46wR-k zRk=cFcI}3ei=s9Sem5kg=S$9#Twt`^dZ9H?tt8M8vCVB$7BDhC*sMOyfJs=KQM03{ QzoN6^D+7>OWD0a00Bg68b^rhX delta 202 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

!1o2o@fw9{K?NYYmtE) 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 diff --git a/routes/launch_api.py b/routes/launch_api.py index ed8daae..24894a5 100644 --- a/routes/launch_api.py +++ b/routes/launch_api.py @@ -13,6 +13,9 @@ from email.utils import formatdate from datetime import datetime import requests import base64 +from flask_wtf.csrf import CSRFProtect +from functools import wraps +import os launch_api = Blueprint('launch_api', __name__) @@ -657,6 +660,60 @@ def download_docker_compose(): if not git_settings: return jsonify({'message': 'Git settings not configured'}), 400 + # Get the current commit hash and latest tag for the branch + commit_hash = None + latest_tag = None + if git_settings['provider'] == 'gitea': + headers = { + 'Accept': 'application/json', + 'Authorization': f'token {git_settings["token"]}' + } + + # Get the latest commit for the branch + commit_response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/commits/{data["branch"]}', + headers=headers + ) + + if commit_response.status_code == 200: + commit_data = commit_response.json() + commit_hash = commit_data.get('sha') + else: + # Try token as query parameter if header auth fails + commit_response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/commits/{data["branch"]}?token={git_settings["token"]}', + headers={'Accept': 'application/json'} + ) + if commit_response.status_code == 200: + commit_data = commit_response.json() + commit_hash = commit_data.get('sha') + + # Get the latest tag + tags_response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/tags', + headers=headers + ) + + if tags_response.status_code == 200: + tags_data = tags_response.json() + if tags_data: + # Sort tags by commit date (newest first) and get the latest + sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True) + if sorted_tags: + latest_tag = sorted_tags[0].get('name') + else: + # Try token as query parameter if header auth fails + tags_response = requests.get( + f'{git_settings["url"]}/api/v1/repos/{data["repository"]}/tags?token={git_settings["token"]}', + headers={'Accept': 'application/json'} + ) + if tags_response.status_code == 200: + tags_data = tags_response.json() + if tags_data: + sorted_tags = sorted(tags_data, key=lambda x: x.get('commit', {}).get('created', ''), reverse=True) + if sorted_tags: + latest_tag = sorted_tags[0].get('name') + # Determine the provider and set up the appropriate API call if git_settings['provider'] == 'gitea': # For Gitea @@ -704,9 +761,53 @@ def download_docker_compose(): else: content = response.text + # Add version.txt creation to the docker-compose content + if commit_hash: + # Create version information with both tag and commit hash + version_info = { + 'tag': latest_tag or 'unknown', + 'commit': commit_hash, + 'branch': data['branch'], + 'deployed_at': datetime.utcnow().isoformat() + } + version_json = json.dumps(version_info, indent=2) + + # Add a command to create version.txt with the version information + version_command = f'echo \'{version_json}\' > /app/version.txt' + + # Find the web service and add the command + if 'web:' in content: + # Add the command to create version.txt before the main command + lines = content.split('\n') + new_lines = [] + in_web_service = False + command_added = False + + for line in lines: + new_lines.append(line) + + if line.strip() == 'web:': + in_web_service = True + elif in_web_service and line.strip().startswith('command:'): + # Add the version.txt creation command before the main command + new_lines.append(f' - sh -c "{version_command} && {line.split("command:")[1].strip()}"') + command_added = True + continue + elif in_web_service and line.strip() and not line.startswith(' ') and not line.startswith('#'): + # We've left the web service section + if not command_added: + # If no command was found, add a new command section + new_lines.append(f' command: sh -c "{version_command} && python app.py"') + command_added = True + in_web_service = False + + content = '\n'.join(new_lines) + return jsonify({ 'success': True, - 'content': content + 'content': content, + 'commit_hash': commit_hash, + 'latest_tag': latest_tag }) else: return jsonify({ @@ -981,26 +1082,77 @@ def save_instance(): missing_fields = [field for field in required_fields if field not in data] return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400 - # Save instance data - instance_data = { - 'name': data['name'], - 'port': data['port'], - 'domains': data['domains'], - 'stack_id': data['stack_id'], - 'stack_name': data['stack_name'], - 'status': data['status'], - 'repository': data['repository'], - 'branch': data['branch'], - 'created_at': datetime.utcnow().isoformat() - } - - # Save to database using KeyValueSettings - KeyValueSettings.set_value(f'instance_{data["name"]}', instance_data) - - return jsonify({ - 'message': 'Instance data saved successfully', - 'data': instance_data - }) + # Check if instance already exists + existing_instance = Instance.query.filter_by(name=data['name']).first() + + if existing_instance: + # Update existing instance + existing_instance.port = data['port'] + existing_instance.domains = data['domains'] + existing_instance.stack_id = data['stack_id'] + existing_instance.stack_name = data['stack_name'] + existing_instance.status = data['status'] + existing_instance.repository = data['repository'] + existing_instance.branch = data['branch'] + existing_instance.deployed_version = data.get('deployed_version', 'unknown') + existing_instance.deployed_branch = data.get('deployed_branch', data['branch']) + existing_instance.version_checked_at = datetime.utcnow() + + db.session.commit() + + return jsonify({ + 'message': 'Instance data updated successfully', + 'data': { + 'name': existing_instance.name, + 'port': existing_instance.port, + 'domains': existing_instance.domains, + 'stack_id': existing_instance.stack_id, + 'stack_name': existing_instance.stack_name, + 'status': existing_instance.status, + 'repository': existing_instance.repository, + 'branch': existing_instance.branch, + 'deployed_version': existing_instance.deployed_version, + 'deployed_branch': existing_instance.deployed_branch + } + }) + else: + # Create new instance + instance = Instance( + name=data['name'], + company='Loading...', # Will be updated later + rooms_count=0, + conversations_count=0, + data_size=0.0, + payment_plan='Basic', + main_url=f"https://{data['domains'][0]}" if data['domains'] else f"http://localhost:{data['port']}", + status=data['status'], + port=data['port'], + stack_id=data['stack_id'], + stack_name=data['stack_name'], + repository=data['repository'], + branch=data['branch'], + deployed_version=data.get('deployed_version', 'unknown'), + deployed_branch=data.get('deployed_branch', data['branch']) + ) + + db.session.add(instance) + db.session.commit() + + return jsonify({ + 'message': 'Instance data saved successfully', + 'data': { + 'name': instance.name, + 'port': instance.port, + 'domains': instance.domains, + 'stack_id': instance.stack_id, + 'stack_name': instance.stack_name, + 'status': instance.status, + 'repository': instance.repository, + 'branch': instance.branch, + 'deployed_version': instance.deployed_version, + 'deployed_branch': instance.deployed_branch + } + }) except Exception as e: current_app.logger.error(f"Error saving instance data: {str(e)}") diff --git a/routes/main.py b/routes/main.py index 846afb4..ba0562f 100644 --- a/routes/main.py +++ b/routes/main.py @@ -379,18 +379,58 @@ def init_routes(main_bp): instances = Instance.query.order_by(Instance.name.asc()).all() - # Check status for each instance + # Get Git settings + git_settings = KeyValueSettings.get_value('git_settings') + gitea_url = git_settings.get('url') if git_settings else None + gitea_token = git_settings.get('token') if git_settings else None + gitea_repo = git_settings.get('repo') if git_settings else None + for instance in instances: + # 1. Check status status_info = check_instance_status(instance) instance.status = status_info['status'] instance.status_details = status_info['details'] + + # 2. Check deployed version + deployed_version = None + deployed_tag = None + deployed_commit = None + try: + version_url = f"{instance.main_url.rstrip('/')}/api/version" + resp = requests.get(version_url, timeout=5) + if resp.status_code == 200: + version_data = resp.json() + deployed_version = version_data.get('version', 'unknown') + deployed_tag = version_data.get('tag', 'unknown') + deployed_commit = version_data.get('commit', 'unknown') + except Exception as e: + deployed_version = None + deployed_tag = None + deployed_commit = None + + instance.deployed_version = deployed_tag or deployed_version or 'unknown' + instance.deployed_branch = instance.deployed_branch or 'master' + + # 3. Check latest version from Gitea (if settings available) + latest_version = None + deployed_branch = instance.deployed_branch or 'master' + if gitea_url and gitea_token and gitea_repo: + try: + headers = {'Accept': 'application/json', 'Authorization': f'token {gitea_token}'} + # Gitea API: /api/v1/repos/{owner}/{repo}/commits/{branch} + commit_url = f"{gitea_url}/api/v1/repos/{gitea_repo}/commits/{deployed_branch}" + commit_resp = requests.get(commit_url, headers=headers, timeout=5) + if commit_resp.status_code == 200: + latest_version = commit_resp.json().get('sha') + except Exception as e: + latest_version = None + instance.latest_version = latest_version or 'unknown' + instance.version_checked_at = datetime.utcnow() db.session.commit() - # Get connection settings portainer_settings = KeyValueSettings.get_value('portainer_settings') nginx_settings = KeyValueSettings.get_value('nginx_settings') - git_settings = KeyValueSettings.get_value('git_settings') cloudflare_settings = KeyValueSettings.get_value('cloudflare_settings') return render_template('main/instances.html', @@ -1975,4 +2015,36 @@ def init_routes(main_bp): flash('This page is only available in master instances.', 'error') return redirect(url_for('main.dashboard')) - return render_template('wiki/base.html') \ No newline at end of file + return render_template('wiki/base.html') + + @main_bp.route('/api/version') + def api_version(): + version_file = os.path.join(current_app.root_path, 'version.txt') + version = 'unknown' + version_data = {} + + if os.path.exists(version_file): + with open(version_file, 'r') as f: + content = f.read().strip() + + # Try to parse as JSON first (new format) + try: + version_data = json.loads(content) + version = version_data.get('tag', 'unknown') + except json.JSONDecodeError: + # Fallback to old format (just commit hash) + version = content + version_data = { + 'tag': 'unknown', + 'commit': content, + 'branch': 'unknown', + 'deployed_at': 'unknown' + } + + return jsonify({ + 'version': version, + 'tag': version_data.get('tag', 'unknown'), + 'commit': version_data.get('commit', 'unknown'), + 'branch': version_data.get('branch', 'unknown'), + 'deployed_at': version_data.get('deployed_at', 'unknown') + }) \ No newline at end of file diff --git a/static/js/launch_progress.js b/static/js/launch_progress.js index 35ad2a6..fa2892a 100644 --- a/static/js/launch_progress.js +++ b/static/js/launch_progress.js @@ -546,12 +546,19 @@ async function startLaunch(data) { stack_name: stackResult.data.name, status: stackResult.data.status, repository: data.repository, - branch: data.branch + branch: data.branch, + deployed_version: dockerComposeResult.latest_tag || dockerComposeResult.commit_hash || 'unknown', + deployed_branch: data.branch }; console.log('Saving instance data:', instanceData); const saveResult = await saveInstanceData(instanceData); console.log('Save result:', saveResult); - await updateStep(10, 'Saving Instance Data', 'Instance data saved successfully'); + + // Update step with version information + const versionInfo = dockerComposeResult.commit_hash ? + `Instance data saved successfully. Version: ${dockerComposeResult.commit_hash.substring(0, 8)}` : + 'Instance data saved successfully'; + await updateStep(10, 'Saving Instance Data', versionInfo); } catch (error) { console.error('Error saving instance data:', error); await updateStep(10, 'Saving Instance Data', `Error: ${error.message}`); diff --git a/templates/main/instances.html b/templates/main/instances.html index 9cc2a95..29d9202 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -3,6 +3,27 @@ {% block title %}Instances - DocuPulse{% endblock %} +{% block extra_css %} + +{% endblock %} + {% block content %} {{ header( title="Instances", @@ -43,6 +64,7 @@ Payment Plan Main URL Status + Version Connection Token Actions @@ -74,6 +96,44 @@ {{ instance.status|title }} + + {% if instance.deployed_version and instance.deployed_version != 'unknown' %} +

+
+ Version: + {{ instance.deployed_version }} +
+ {% if instance.latest_version and instance.latest_version != 'unknown' %} +
+ Latest: + {{ instance.latest_version }} +
+ {% if instance.deployed_version == instance.latest_version %} + + Up-to-date + + {% else %} + + Outdated + + {% endif %} + {% else %} + + Unknown + + {% endif %} + {% if instance.version_checked_at %} +
+ {{ instance.version_checked_at.strftime('%H:%M') }} +
+ {% endif %} +
+ {% else %} + + Unknown + + {% endif %} + {% if instance.connection_token %} diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..e1fa3e4 --- /dev/null +++ b/version.txt @@ -0,0 +1,6 @@ +{ + "tag": "v1.2.3", + "commit": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0", + "branch": "main", + "deployed_at": "2024-01-15T10:30:00.000000" +} \ No newline at end of file