Refactor backend and add registry build workflow
Build and publish Docker image / build-and-publish (push) Successful in 2m45s

This commit is contained in:
wfalkowski committed 2026-10-20 16:44:49 +00:00
commit c177d7981d
43 files changed
+18124

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
"""Server demo operations."""
from database_service import database_connection, dict_from_row
from kubernetes import client
from kubernetes_service import k8s_manager
from runtime_config import (
DATABASE_URL,
logger,
)
from server_capacity import calculate_server_cost
from typing import Dict
from user_service import add_credit_transaction, get_user_by_id, is_demo_enabled, update_user_credits
import uuid
__all__ = ['check_demo_eligibility', 'cleanup_expired_demo_servers', 'convert_demo_to_paid', 'mark_demo_used']
def check_demo_eligibility(user_id: str) -> Dict:
"""Check if user is eligible for demo server"""
try:
if not is_demo_enabled():
return {
'eligible': False,
'has_used_demo': False,
'has_purchased_credits': False,
'reason': 'DEMO servers are currently disabled'
}
with database_connection() as conn:
cursor = conn.cursor()
# Check if user has used demo before
cursor.execute('SELECT id FROM demo_servers_used WHERE user_id = ?', (user_id,))
has_used_demo = cursor.fetchone() is not None
# Check if user has purchased credits (type='purchase')
cursor.execute('''
SELECT COUNT(*) FROM credit_transactions
WHERE user_id = ? AND type = 'purchase'
''', (user_id,))
purchase_count = cursor.fetchone()['count']
has_purchased_credits = purchase_count > 0
eligible = not has_used_demo and not has_purchased_credits
return {
'eligible': eligible,
'has_used_demo': has_used_demo,
'has_purchased_credits': has_purchased_credits,
'reason': None if eligible else (
'You have already used the demo server' if has_used_demo
else 'You have purchased credits before'
)
}
except Exception as e:
logger.error(f"Error checking demo eligibility: {e}", exc_info=True)
return {'eligible': False, 'has_used_demo': False, 'has_purchased_credits': False, 'reason': 'Error checking eligibility'}
def mark_demo_used(user_id: str) -> bool:
"""Mark that user has used demo server"""
try:
with database_connection() as conn:
cursor = conn.cursor()
demo_id = f"demo-{str(uuid.uuid4())[:8]}"
# PostgreSQL-compatible syntax
if 'postgres' in DATABASE_URL:
cursor.execute('''
INSERT INTO demo_servers_used (id, user_id)
VALUES (%s, %s)
ON CONFLICT (user_id) DO NOTHING
''', (demo_id, user_id))
else:
# SQLite syntax
cursor.execute('''
INSERT OR IGNORE INTO demo_servers_used (id, user_id)
VALUES (?, ?)
''', (demo_id, user_id))
conn.commit()
return True
except Exception as e:
logger.error(f"Error marking demo as used: {e}")
return False
def convert_demo_to_paid(server_id: str, user_id: str) -> Dict:
"""Convert demo server to paid server"""
try:
with database_connection() as conn:
cursor = conn.cursor()
# Get server details
cursor.execute('''
SELECT * FROM servers WHERE id = ? AND owner_id = ?
''', (server_id, user_id))
server = dict_from_row(cursor.fetchone())
if not server:
raise Exception('Server not found')
if not server.get('is_demo'):
raise Exception('Server is not a demo server')
# Calculate server cost
config = {
'serverType': server['type'],
'cpu': server['cpu'],
'ram': server['ram'],
'storage': server['storage']
}
server_cost = calculate_server_cost(config)
# Check user credits
user = get_user_by_id(user_id)
if user.get('credits', 0) < server_cost:
raise Exception(f'Insufficient credits. Need {server_cost}, have {user.get("credits", 0)}')
# Deduct credits
new_credits = user.get('credits', 0) - server_cost
if not update_user_credits(user_id, new_credits):
raise Exception('Failed to deduct credits')
add_credit_transaction(user_id, -server_cost, 'deduct', f'Convert demo server to paid: {server["name"]}')
# Update server to remove demo status
cursor.execute('''
UPDATE servers
SET is_demo = FALSE, demo_expires_at = NULL, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (server_id,))
conn.commit()
logger.info(f"✓ Converted demo server {server_id} to paid server")
return {
'success': True,
'server_id': server_id,
'credits_charged': server_cost,
'new_balance': new_credits
}
except Exception as e:
logger.error(f"Error converting demo to paid: {e}")
raise Exception(f'Failed to convert demo server: {str(e)}')
def cleanup_expired_demo_servers():
"""Delete expired demo servers"""
try:
with database_connection() as conn:
cursor = conn.cursor()
# Find expired demo servers
cursor.execute('''
SELECT id, name, owner_id, namespace, type, k8s_name FROM servers
WHERE is_demo = TRUE
AND demo_expires_at IS NOT NULL
AND demo_expires_at < CURRENT_TIMESTAMP
''')
expired_servers = cursor.fetchall()
deleted_count = 0
for row in expired_servers:
server_id = row[0]
server_name = row[1]
owner_id = row[2]
namespace = row[3] if len(row) > 3 else None
server_type = row[4] if len(row) > 4 else 'vps'
k8s_name = row[5] if len(row) > 5 else server_id # Use k8s_name if available, fallback to server_id
try:
# Delete from Kubernetes first if connected
if k8s_manager.connected and namespace:
try:
logger.info(f"Attempting to delete Kubernetes resources for expired demo server {server_id} (k8s_name: {k8s_name}) in namespace {namespace}")
# Import here to avoid issues if k8s not connected
from kubernetes.client.rest import ApiException
apps_v1 = client.AppsV1Api()
v1 = client.CoreV1Api()
# Try to delete deployment using k8s_name
try:
apps_v1.delete_namespaced_deployment(name=k8s_name, namespace=namespace)
logger.info(f"✓ Deleted deployment for demo server {server_id} (k8s_name: {k8s_name})")
except ApiException as e:
if e.status != 404: # 404 means it doesn't exist, which is fine
logger.warning(f"Could not delete deployment for {server_id} (k8s_name: {k8s_name}): {e}")
# Try to delete service
try:
v1.delete_namespaced_service(name=f"{k8s_name}-service", namespace=namespace)
logger.info(f"✓ Deleted service for demo server {server_id} (k8s_name: {k8s_name})")
except ApiException as e:
if e.status != 404:
logger.warning(f"Could not delete service for {server_id} (k8s_name: {k8s_name}): {e}")
except Exception as k8s_error:
logger.error(f"Error deleting Kubernetes resources for {server_id}: {k8s_error}")
# Delete server from database (this will cascade delete related records)
cursor.execute('DELETE FROM servers WHERE id = ?', (server_id,))
conn.commit()
deleted_count += 1
logger.info(f"✓ Deleted expired demo server: {server_name} ({server_id})")
except Exception as e:
logger.error(f"Error deleting expired demo server {server_id}: {e}")
if deleted_count > 0:
logger.info(f"✓ Cleaned up {deleted_count} expired demo server(s)")
return deleted_count
except Exception as e:
logger.error(f"Error cleaning up expired demo servers: {e}")
return 0