xenfra 0.2.2__py3-none-any.whl → 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xenfra/__init__.py +0 -1
- xenfra/commands/__init__.py +3 -0
- xenfra/commands/auth.py +186 -0
- xenfra/commands/deployments.py +443 -0
- xenfra/commands/intelligence.py +312 -0
- xenfra/commands/projects.py +163 -0
- xenfra/commands/security_cmd.py +235 -0
- xenfra/main.py +70 -0
- xenfra/utils/__init__.py +3 -0
- xenfra/utils/auth.py +148 -0
- xenfra/utils/codebase.py +86 -0
- xenfra/utils/config.py +278 -0
- xenfra/utils/security.py +350 -0
- xenfra-0.2.4.dist-info/METADATA +115 -0
- xenfra-0.2.4.dist-info/RECORD +17 -0
- xenfra-0.2.4.dist-info/entry_points.txt +3 -0
- xenfra/api/auth.py +0 -51
- xenfra/api/billing.py +0 -80
- xenfra/api/connections.py +0 -163
- xenfra/api/main.py +0 -175
- xenfra/api/webhooks.py +0 -146
- xenfra/cli/main.py +0 -211
- xenfra/config.py +0 -24
- xenfra/db/models.py +0 -51
- xenfra/db/session.py +0 -17
- xenfra/dependencies.py +0 -35
- xenfra/dockerizer.py +0 -89
- xenfra/engine.py +0 -293
- xenfra/mcp_client.py +0 -149
- xenfra/models.py +0 -54
- xenfra/recipes.py +0 -23
- xenfra/security.py +0 -58
- xenfra/templates/Dockerfile.j2 +0 -25
- xenfra/templates/cloud-init.sh.j2 +0 -68
- xenfra/templates/docker-compose.yml.j2 +0 -33
- xenfra/utils.py +0 -69
- xenfra-0.2.2.dist-info/METADATA +0 -95
- xenfra-0.2.2.dist-info/RECORD +0 -25
- xenfra-0.2.2.dist-info/entry_points.txt +0 -3
- {xenfra-0.2.2.dist-info → xenfra-0.2.4.dist-info}/WHEEL +0 -0
xenfra/api/webhooks.py
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
# src/xenfra/api/webhooks.py
|
|
2
|
-
|
|
3
|
-
import hmac
|
|
4
|
-
import hashlib
|
|
5
|
-
from fastapi import APIRouter, Depends, HTTPException, status, Request, BackgroundTasks
|
|
6
|
-
from sqlmodel import Session, select
|
|
7
|
-
|
|
8
|
-
from xenfra.db.session import get_session
|
|
9
|
-
from xenfra.db.models import User, Credential
|
|
10
|
-
from xenfra.dependencies import get_current_active_user # Corrected import
|
|
11
|
-
from xenfra.engine import InfraEngine
|
|
12
|
-
from xenfra.security import decrypt_token
|
|
13
|
-
from xenfra.config import settings
|
|
14
|
-
|
|
15
|
-
router = APIRouter()
|
|
16
|
-
|
|
17
|
-
# This secret should be configured in your GitHub App's webhook settings
|
|
18
|
-
GITHUB_WEBHOOK_SECRET = settings.GITHUB_WEBHOOK_SECRET
|
|
19
|
-
|
|
20
|
-
async def verify_github_signature(request: Request):
|
|
21
|
-
"""
|
|
22
|
-
Verify that the incoming webhook request is genuinely from GitHub.
|
|
23
|
-
"""
|
|
24
|
-
if not GITHUB_WEBHOOK_SECRET:
|
|
25
|
-
raise HTTPException(status_code=500, detail="GitHub webhook secret not configured.")
|
|
26
|
-
|
|
27
|
-
signature_header = request.headers.get("X-Hub-Signature-256")
|
|
28
|
-
if not signature_header:
|
|
29
|
-
raise HTTPException(status_code=400, detail="X-Hub-Signature-256 header is missing.")
|
|
30
|
-
|
|
31
|
-
signature_parts = signature_header.split("=", 1)
|
|
32
|
-
if len(signature_parts) != 2 or signature_parts[0] != "sha256":
|
|
33
|
-
raise HTTPException(status_code=400, detail="Invalid signature format.")
|
|
34
|
-
|
|
35
|
-
signature = signature_parts[1]
|
|
36
|
-
body = await request.body()
|
|
37
|
-
|
|
38
|
-
expected_signature = hmac.new(
|
|
39
|
-
key=GITHUB_WEBHOOK_SECRET.encode(),
|
|
40
|
-
msg=body,
|
|
41
|
-
digestmod=hashlib.sha256
|
|
42
|
-
).hexdigest()
|
|
43
|
-
|
|
44
|
-
if not hmac.compare_digest(expected_signature, signature):
|
|
45
|
-
raise HTTPException(status_code=400, detail="Invalid signature.")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@router.post("/github", dependencies=[Depends(verify_github_signature)])
|
|
49
|
-
async def github_webhook(request: Request, background_tasks: BackgroundTasks, session: Session = Depends(get_session)):
|
|
50
|
-
"""
|
|
51
|
-
Handles incoming webhooks from GitHub to manage Preview Environments.
|
|
52
|
-
"""
|
|
53
|
-
payload = await request.json()
|
|
54
|
-
event_type = request.headers.get("X-GitHub-Event")
|
|
55
|
-
|
|
56
|
-
if event_type == "pull_request":
|
|
57
|
-
action = payload.get("action")
|
|
58
|
-
pr_info = payload.get("pull_request", {})
|
|
59
|
-
repo_info = payload.get("repository", {})
|
|
60
|
-
|
|
61
|
-
repo_full_name = repo_info.get("full_name")
|
|
62
|
-
pr_number = pr_info.get("number")
|
|
63
|
-
commit_sha = pr_info.get("head", {}).get("sha")
|
|
64
|
-
clone_url = repo_info.get("clone_url")
|
|
65
|
-
installation_id = payload.get("installation", {}).get("id")
|
|
66
|
-
|
|
67
|
-
if not all([repo_full_name, pr_number, commit_sha, clone_url, installation_id]):
|
|
68
|
-
raise HTTPException(status_code=400, detail="Incomplete pull request payload or missing installation ID.")
|
|
69
|
-
|
|
70
|
-
# Find the user associated with this GitHub App installation
|
|
71
|
-
github_credential = session.exec(
|
|
72
|
-
select(Credential).where(
|
|
73
|
-
Credential.service == "github",
|
|
74
|
-
Credential.github_installation_id == installation_id
|
|
75
|
-
)
|
|
76
|
-
).first()
|
|
77
|
-
|
|
78
|
-
if not github_credential:
|
|
79
|
-
raise HTTPException(status_code=404, detail=f"No GitHub credential found for installation ID {installation_id}.")
|
|
80
|
-
|
|
81
|
-
user = session.exec(select(User).where(User.id == github_credential.user_id)).first()
|
|
82
|
-
if not user:
|
|
83
|
-
raise HTTPException(status_code=404, detail=f"User not found for credential with installation ID {installation_id}.")
|
|
84
|
-
|
|
85
|
-
# Find the user's DigitalOcean credential
|
|
86
|
-
do_credential = session.exec(
|
|
87
|
-
select(Credential).where(Credential.user_id == user.id, Credential.service == "digitalocean")
|
|
88
|
-
).first()
|
|
89
|
-
|
|
90
|
-
if not do_credential:
|
|
91
|
-
raise HTTPException(status_code=400, detail=f"No DigitalOcean credential found for user {user.email}.")
|
|
92
|
-
|
|
93
|
-
# Decrypt the token and instantiate the engine
|
|
94
|
-
try:
|
|
95
|
-
do_token = decrypt_token(do_credential.encrypted_token)
|
|
96
|
-
engine = InfraEngine(token=do_token) # InfraEngine needs to be adapted to accept a token
|
|
97
|
-
|
|
98
|
-
print(f"DEBUG(WEBHOOKS): engine object id: {id(engine)}, type: {type(engine)}")
|
|
99
|
-
print(f"DEBUG(WEBHOOKS): engine.list_servers method id: {id(engine.list_servers)}, type: {type(engine.list_servers)}")
|
|
100
|
-
|
|
101
|
-
except Exception as e:
|
|
102
|
-
return {"status": "error", "detail": f"Failed to initialize engine: {e}"}
|
|
103
|
-
|
|
104
|
-
server_name = f"xenfra-pr-{repo_full_name.replace('/', '-')}-{pr_number}"
|
|
105
|
-
|
|
106
|
-
if action in ["opened", "synchronize"]:
|
|
107
|
-
print(f"🚀 Deploying preview for PR #{pr_number} from {repo_full_name} at {commit_sha[:7]}")
|
|
108
|
-
# This should be run in a background task in a real app
|
|
109
|
-
background_tasks.add_task(
|
|
110
|
-
engine.deploy_server,
|
|
111
|
-
name=server_name,
|
|
112
|
-
region="nyc3", # Using a default for now
|
|
113
|
-
size="s-1vcpu-1gb", # Using a default for now
|
|
114
|
-
image="ubuntu-22-04-x64", # Using a default for now
|
|
115
|
-
email=user.email,
|
|
116
|
-
repo_url=clone_url,
|
|
117
|
-
commit_sha=commit_sha
|
|
118
|
-
)
|
|
119
|
-
# TODO: Post comment back to GitHub with the preview URL
|
|
120
|
-
# TODO: Store the droplet_id and PR number association in the DB
|
|
121
|
-
|
|
122
|
-
return {"status": "success", "action": "deploying in background"}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
elif action == "closed":
|
|
126
|
-
print(f"🔥 Destroying preview for closed PR #{pr_number} from {repo_full_name}")
|
|
127
|
-
try:
|
|
128
|
-
# Find the droplet associated with this PR
|
|
129
|
-
servers = engine.list_servers()
|
|
130
|
-
droplet_to_destroy = None
|
|
131
|
-
for s in servers:
|
|
132
|
-
if s.name == server_name:
|
|
133
|
-
droplet_to_destroy = s
|
|
134
|
-
break
|
|
135
|
-
|
|
136
|
-
if droplet_to_destroy:
|
|
137
|
-
background_tasks.add_task(engine.destroy_server, droplet_to_destroy.id)
|
|
138
|
-
print(f"✅ Droplet {droplet_to_destroy.name} ({droplet_to_destroy.id}) scheduled for destruction.")
|
|
139
|
-
else:
|
|
140
|
-
print(f"⚠️ No droplet found with name {server_name} to destroy.")
|
|
141
|
-
except Exception as e:
|
|
142
|
-
print(f"❌ Failed to destroy preview environment: {e}")
|
|
143
|
-
|
|
144
|
-
return {"status": "success", "action": "destroying in background"}
|
|
145
|
-
|
|
146
|
-
return {"status": "success", "detail": "Webhook received"}
|
xenfra/cli/main.py
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import click
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.table import Table
|
|
4
|
-
|
|
5
|
-
from xenfra.engine import InfraEngine
|
|
6
|
-
from xenfra import dockerizer
|
|
7
|
-
import yaml
|
|
8
|
-
|
|
9
|
-
console = Console()
|
|
10
|
-
|
|
11
|
-
@click.group()
|
|
12
|
-
@click.pass_context
|
|
13
|
-
def main(ctx):
|
|
14
|
-
"""
|
|
15
|
-
Xenfra CLI: A 'Zen Mode' infrastructure engine for Python developers.
|
|
16
|
-
"""
|
|
17
|
-
try:
|
|
18
|
-
ctx.obj = {'engine': InfraEngine()}
|
|
19
|
-
user_info = ctx.obj['engine'].get_user_info()
|
|
20
|
-
console.print(f"[bold underline]Xenfra CLI[/bold underline] - Logged in as [green]{user_info.email}[/green]")
|
|
21
|
-
except Exception as e:
|
|
22
|
-
console.print(f"[bold red]CRITICAL ERROR:[/bold red] Failed to initialize engine: {e}")
|
|
23
|
-
exit(1)
|
|
24
|
-
|
|
25
|
-
@main.command()
|
|
26
|
-
@click.pass_context
|
|
27
|
-
def init(ctx):
|
|
28
|
-
"""Initializes a project by creating a xenfra.yaml configuration file."""
|
|
29
|
-
console.print("\n[bold blue]🔎 INITIALIZING PROJECT[/bold blue]")
|
|
30
|
-
|
|
31
|
-
framework, _, _ = dockerizer.detect_framework()
|
|
32
|
-
if not framework:
|
|
33
|
-
console.print("[yellow] Warning: No recognizable web framework detected.[/yellow]")
|
|
34
|
-
|
|
35
|
-
console.print(f" - Detected [cyan]{framework or 'unknown'}[/cyan] project.")
|
|
36
|
-
|
|
37
|
-
use_db = click.confirm("\n Would you like to add a PostgreSQL database to your deployment?", default=False)
|
|
38
|
-
|
|
39
|
-
config = {
|
|
40
|
-
'name': 'xenfra-app',
|
|
41
|
-
'digitalocean': {
|
|
42
|
-
'region': 'nyc3',
|
|
43
|
-
'size': 's-1vcpu-1gb',
|
|
44
|
-
'image': 'ubuntu-22-04-x64'
|
|
45
|
-
},
|
|
46
|
-
'app': {
|
|
47
|
-
'framework': framework
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if use_db:
|
|
52
|
-
config['database'] = {
|
|
53
|
-
'type': 'postgres',
|
|
54
|
-
'user': 'db_user',
|
|
55
|
-
'password': 'db_password', # In a real scenario, this should be handled more securely
|
|
56
|
-
'name': 'app_db'
|
|
57
|
-
}
|
|
58
|
-
console.print(" - Added [bold green]PostgreSQL[/bold green] to the configuration.")
|
|
59
|
-
|
|
60
|
-
with open('xenfra.yaml', 'w') as f:
|
|
61
|
-
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
62
|
-
|
|
63
|
-
console.print("\n[bold green]✅ SUCCESS![/bold green]")
|
|
64
|
-
console.print(" - Created [cyan]xenfra.yaml[/cyan].")
|
|
65
|
-
console.print("\n Next step: Review the configuration and run 'xenfra deploy'!")
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@main.command()
|
|
69
|
-
@click.pass_context
|
|
70
|
-
def deploy(ctx):
|
|
71
|
-
"""Deploys the project based on the xenfra.yaml configuration."""
|
|
72
|
-
console.print("\n[bold green]🚀 INITIATING DEPLOYMENT FROM CONFIGURATION[/bold green]")
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
with open('xenfra.yaml', 'r') as f:
|
|
76
|
-
config = yaml.safe_load(f)
|
|
77
|
-
except FileNotFoundError:
|
|
78
|
-
raise click.ClickException("No 'xenfra.yaml' found. Run 'xenfra init' to create a configuration file.")
|
|
79
|
-
|
|
80
|
-
engine = ctx.obj['engine']
|
|
81
|
-
|
|
82
|
-
# Extract config values
|
|
83
|
-
name = config.get('name', 'xenfra-app')
|
|
84
|
-
do_config = config.get('digitalocean', {})
|
|
85
|
-
region = do_config.get('region', 'nyc3')
|
|
86
|
-
size = do_config.get('size', 's-1vcpu-1gb')
|
|
87
|
-
image = do_config.get('image', 'ubuntu-22-04-x64')
|
|
88
|
-
|
|
89
|
-
# Build context for templates
|
|
90
|
-
template_context = {
|
|
91
|
-
'database': config.get('database', {}).get('type'),
|
|
92
|
-
'db_user': config.get('database', {}).get('user'),
|
|
93
|
-
'db_password': config.get('database', {}).get('password'),
|
|
94
|
-
'db_name': config.get('database', {}).get('name'),
|
|
95
|
-
'email': ctx.obj['engine'].get_user_info().email
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.print(f" - App Name: [cyan]{name}[/cyan]")
|
|
99
|
-
console.print(f" - Region: [cyan]{region}[/cyan], Size: [cyan]{size}[/cyan]")
|
|
100
|
-
if template_context.get('database'):
|
|
101
|
-
console.print(f" - Including Database: [cyan]{template_context['database']}[/cyan]")
|
|
102
|
-
|
|
103
|
-
if not click.confirm(f"\n Ready to deploy '{name}' from 'xenfra.yaml'?"):
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
with console.status("[bold green]Deployment in progress...[/bold green]"):
|
|
107
|
-
result = engine.deploy_server(
|
|
108
|
-
name=name,
|
|
109
|
-
region=region,
|
|
110
|
-
size=size,
|
|
111
|
-
image=image,
|
|
112
|
-
logger=console.log,
|
|
113
|
-
**template_context
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
console.print(f"\n[bold green]✅ DEPLOYMENT COMPLETE![/bold green]")
|
|
117
|
-
console.print(result)
|
|
118
|
-
|
|
119
|
-
@main.command(name="list")
|
|
120
|
-
@click.option('--refresh', is_flag=True, help="Sync with the cloud provider before listing.")
|
|
121
|
-
@click.pass_context
|
|
122
|
-
def list_projects(ctx, refresh):
|
|
123
|
-
"""Lists all active Xenfra projects from the local database."""
|
|
124
|
-
engine = ctx.obj['engine']
|
|
125
|
-
|
|
126
|
-
if refresh:
|
|
127
|
-
console.print("\n[bold]📡 SYNCING WITH CLOUD PROVIDER...[/bold]")
|
|
128
|
-
with console.status("Calling DigitalOcean API and reconciling state..."):
|
|
129
|
-
projects = engine.sync_with_provider()
|
|
130
|
-
else:
|
|
131
|
-
console.print("\n[bold]⚡️ LISTING PROJECTS FROM LOCAL DATABASE[/bold]")
|
|
132
|
-
projects = engine.list_projects_from_db()
|
|
133
|
-
|
|
134
|
-
if not projects:
|
|
135
|
-
console.print("[yellow] No active projects found. Run 'xenfra deploy' to create one.[/yellow]")
|
|
136
|
-
else:
|
|
137
|
-
table = Table(show_header=True, header_style="bold magenta")
|
|
138
|
-
table.add_column("Droplet ID", style="dim", width=12)
|
|
139
|
-
table.add_column("Name", style="cyan")
|
|
140
|
-
table.add_column("IP Address", style="green")
|
|
141
|
-
table.add_column("Status")
|
|
142
|
-
table.add_column("Region")
|
|
143
|
-
table.add_column("Size")
|
|
144
|
-
for p in projects:
|
|
145
|
-
table.add_row(str(p.droplet_id), p.name, p.ip_address, p.status, p.region, p.size)
|
|
146
|
-
console.print(table)
|
|
147
|
-
|
|
148
|
-
@main.command(name="logs")
|
|
149
|
-
@click.pass_context
|
|
150
|
-
def logs(ctx):
|
|
151
|
-
"""Streams real-time logs from a deployed project."""
|
|
152
|
-
engine = ctx.obj['engine']
|
|
153
|
-
|
|
154
|
-
console.print("\n[bold yellow]📡 SELECT A PROJECT TO STREAM LOGS[/bold yellow]")
|
|
155
|
-
projects = engine.list_projects_from_db()
|
|
156
|
-
|
|
157
|
-
if not projects:
|
|
158
|
-
console.print("[yellow] No active projects to stream logs from.[/yellow]")
|
|
159
|
-
return
|
|
160
|
-
|
|
161
|
-
project_map = {str(i+1): p for i, p in enumerate(projects)}
|
|
162
|
-
for k, p in project_map.items():
|
|
163
|
-
console.print(f" [{k}] {p.name} ({p.ip_address})")
|
|
164
|
-
|
|
165
|
-
choice_key = click.prompt("\n Select Project (0 to cancel)", type=click.Choice(['0'] + list(project_map.keys())), show_choices=False)
|
|
166
|
-
if choice_key == '0':
|
|
167
|
-
return
|
|
168
|
-
|
|
169
|
-
target = project_map[choice_key]
|
|
170
|
-
|
|
171
|
-
try:
|
|
172
|
-
console.print(f"\n[bold green]-- Attaching to logs for {target.name} (Press Ctrl+C to stop) --[/bold green]")
|
|
173
|
-
engine.stream_logs(target.droplet_id)
|
|
174
|
-
except DeploymentError as e:
|
|
175
|
-
console.print(f"[bold red]ERROR:[/bold red] {e.message}")
|
|
176
|
-
except KeyboardInterrupt:
|
|
177
|
-
console.print("\n[bold yellow]-- Log streaming stopped by user. --[/bold yellow]")
|
|
178
|
-
except Exception as e:
|
|
179
|
-
console.print(f"[bold red]An unexpected error occurred:[/bold red] {e}")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
@main.command()
|
|
183
|
-
@click.pass_context
|
|
184
|
-
def destroy(ctx):
|
|
185
|
-
"""Destroys a deployed project."""
|
|
186
|
-
engine = ctx.obj['engine']
|
|
187
|
-
|
|
188
|
-
console.print("\n[bold red]🧨 SELECT A PROJECT TO DESTROY[/bold red]")
|
|
189
|
-
projects = engine.list_projects_from_db()
|
|
190
|
-
|
|
191
|
-
if not projects:
|
|
192
|
-
console.print("[yellow] No active projects to destroy.[/yellow]")
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
project_map = {str(i+1): p for i, p in enumerate(projects)}
|
|
196
|
-
for k, p in project_map.items():
|
|
197
|
-
console.print(f" [{k}] {p.name} ({p.ip_address})")
|
|
198
|
-
|
|
199
|
-
choice_key = click.prompt("\n Select Project to DESTROY (0 to cancel)", type=click.Choice(['0'] + list(project_map.keys())), show_choices=False)
|
|
200
|
-
if choice_key == '0':
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
target = project_map[choice_key]
|
|
204
|
-
|
|
205
|
-
if click.confirm(f" Are you SURE you want to permanently delete [red]{target.name}[/red] (Droplet ID: {target.droplet_id})? This action cannot be undone."):
|
|
206
|
-
with console.status(f"💥 Destroying {target.name}..."):
|
|
207
|
-
engine.destroy_server(target.droplet_id)
|
|
208
|
-
console.print(f"[green] Project '{target.name}' has been destroyed.[/green]")
|
|
209
|
-
|
|
210
|
-
if __name__ == "__main__":
|
|
211
|
-
main()
|
xenfra/config.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# src/xenfra/config.py
|
|
2
|
-
|
|
3
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
4
|
-
from typing import Optional
|
|
5
|
-
|
|
6
|
-
class Settings(BaseSettings):
|
|
7
|
-
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
8
|
-
|
|
9
|
-
SECRET_KEY: str
|
|
10
|
-
ENCRYPTION_KEY: str
|
|
11
|
-
|
|
12
|
-
GITHUB_CLIENT_ID: str
|
|
13
|
-
GITHUB_CLIENT_SECRET: str
|
|
14
|
-
GITHUB_REDIRECT_URI: str
|
|
15
|
-
GITHUB_WEBHOOK_SECRET: str
|
|
16
|
-
|
|
17
|
-
DO_CLIENT_ID: str
|
|
18
|
-
DO_CLIENT_SECRET: str
|
|
19
|
-
DO_REDIRECT_URI: str
|
|
20
|
-
|
|
21
|
-
# Frontend redirect for successful OAuth (e.g., /dashboard/connections)
|
|
22
|
-
FRONTEND_OAUTH_REDIRECT_SUCCESS: str = "/dashboard/connections"
|
|
23
|
-
|
|
24
|
-
settings = Settings()
|
xenfra/db/models.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# src/xenfra/db/models.py
|
|
2
|
-
|
|
3
|
-
from typing import List, Optional
|
|
4
|
-
from sqlmodel import Field, Relationship, SQLModel
|
|
5
|
-
|
|
6
|
-
class UserBase(SQLModel):
|
|
7
|
-
email: str = Field(unique=True, index=True)
|
|
8
|
-
is_active: bool = True
|
|
9
|
-
|
|
10
|
-
class User(UserBase, table=True):
|
|
11
|
-
id: Optional[int] = Field(default=None, primary_key=True)
|
|
12
|
-
hashed_password: str
|
|
13
|
-
|
|
14
|
-
credentials: List["Credential"] = Relationship(back_populates="user")
|
|
15
|
-
|
|
16
|
-
class UserCreate(UserBase):
|
|
17
|
-
password: str
|
|
18
|
-
|
|
19
|
-
class UserRead(UserBase):
|
|
20
|
-
id: int
|
|
21
|
-
|
|
22
|
-
class CredentialBase(SQLModel):
|
|
23
|
-
service: str # e.g., "digitalocean", "github"
|
|
24
|
-
encrypted_token: str
|
|
25
|
-
user_id: int = Field(foreign_key="user.id")
|
|
26
|
-
github_installation_id: Optional[int] = Field(default=None, index=True)
|
|
27
|
-
|
|
28
|
-
class Credential(CredentialBase, table=True):
|
|
29
|
-
id: Optional[int] = Field(default=None, primary_key=True)
|
|
30
|
-
|
|
31
|
-
user: User = Relationship(back_populates="credentials")
|
|
32
|
-
|
|
33
|
-
class CredentialCreate(CredentialBase):
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
class CredentialRead(SQLModel):
|
|
37
|
-
id: int
|
|
38
|
-
service: str
|
|
39
|
-
user_id: int
|
|
40
|
-
|
|
41
|
-
# --- Project Model for CLI state ---
|
|
42
|
-
|
|
43
|
-
class Project(SQLModel, table=True):
|
|
44
|
-
id: Optional[int] = Field(default=None, primary_key=True)
|
|
45
|
-
droplet_id: int = Field(unique=True, index=True)
|
|
46
|
-
name: str
|
|
47
|
-
ip_address: str
|
|
48
|
-
status: str
|
|
49
|
-
region: str
|
|
50
|
-
size: str
|
|
51
|
-
|
xenfra/db/session.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# src/xenfra/db/session.py
|
|
2
|
-
|
|
3
|
-
from sqlmodel import create_engine, Session, SQLModel
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
# For now, we will use a simple SQLite database for ease of setup.
|
|
7
|
-
# In production, this should be a PostgreSQL database URL from environment variables.
|
|
8
|
-
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./xenfra.db")
|
|
9
|
-
|
|
10
|
-
engine = create_engine(DATABASE_URL, echo=True)
|
|
11
|
-
|
|
12
|
-
def create_db_and_tables():
|
|
13
|
-
SQLModel.metadata.create_all(engine)
|
|
14
|
-
|
|
15
|
-
def get_session():
|
|
16
|
-
with Session(engine) as session:
|
|
17
|
-
yield session
|
xenfra/dependencies.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# src/xenfra/dependencies.py
|
|
2
|
-
|
|
3
|
-
from fastapi import Depends, HTTPException, status
|
|
4
|
-
from fastapi.security import OAuth2PasswordBearer
|
|
5
|
-
from sqlmodel import Session, select
|
|
6
|
-
|
|
7
|
-
from xenfra.db.session import get_session
|
|
8
|
-
from xenfra.db.models import User
|
|
9
|
-
from xenfra.security import decode_token
|
|
10
|
-
|
|
11
|
-
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
|
|
12
|
-
|
|
13
|
-
def get_current_user(token: str = Depends(oauth2_scheme), session: Session = Depends(get_session)) -> User:
|
|
14
|
-
credentials_exception = HTTPException(
|
|
15
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
16
|
-
detail="Could not validate credentials",
|
|
17
|
-
headers={"WWW-Authenticate": "Bearer"},
|
|
18
|
-
)
|
|
19
|
-
payload = decode_token(token)
|
|
20
|
-
if payload is None:
|
|
21
|
-
raise credentials_exception
|
|
22
|
-
|
|
23
|
-
email: str = payload.get("sub")
|
|
24
|
-
if email is None:
|
|
25
|
-
raise credentials_exception
|
|
26
|
-
|
|
27
|
-
user = session.exec(select(User).where(User.email == email)).first()
|
|
28
|
-
if user is None:
|
|
29
|
-
raise credentials_exception
|
|
30
|
-
return user
|
|
31
|
-
|
|
32
|
-
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
|
|
33
|
-
if not current_user.is_active:
|
|
34
|
-
raise HTTPException(status_code=400, detail="Inactive user")
|
|
35
|
-
return current_user
|
xenfra/dockerizer.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from jinja2 import Environment, FileSystemLoader
|
|
4
|
-
|
|
5
|
-
def detect_framework(path="."):
|
|
6
|
-
"""
|
|
7
|
-
Scans common Python project structures to guess the framework and entrypoint.
|
|
8
|
-
Returns: (framework_name, default_port, start_command) or (None, None, None)
|
|
9
|
-
"""
|
|
10
|
-
project_root = Path(path).resolve()
|
|
11
|
-
|
|
12
|
-
# Check for Django first (common pattern: manage.py in root)
|
|
13
|
-
if (project_root / "manage.py").is_file():
|
|
14
|
-
project_name = project_root.name
|
|
15
|
-
return "django", 8000, f"gunicorn {project_name}.wsgi:application --bind 0.0.0.0:8000"
|
|
16
|
-
|
|
17
|
-
candidate_files = []
|
|
18
|
-
|
|
19
|
-
# Check directly in project root
|
|
20
|
-
for name in ["main.py", "app.py"]:
|
|
21
|
-
if (project_root / name).is_file():
|
|
22
|
-
candidate_files.append(project_root / name)
|
|
23
|
-
|
|
24
|
-
# Check in src/*/ (standard package layout)
|
|
25
|
-
for src_dir in project_root.glob("src/*"):
|
|
26
|
-
if src_dir.is_dir():
|
|
27
|
-
for name in ["main.py", "app.py"]:
|
|
28
|
-
if (src_dir / name).is_file():
|
|
29
|
-
candidate_files.append(src_dir / name)
|
|
30
|
-
|
|
31
|
-
for file_path in candidate_files:
|
|
32
|
-
with open(file_path, "r") as f:
|
|
33
|
-
content = f.read()
|
|
34
|
-
|
|
35
|
-
module_name = str(file_path.relative_to(project_root)).replace(os.sep, '.')[:-3]
|
|
36
|
-
if module_name.startswith("src."):
|
|
37
|
-
module_name = module_name[4:]
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if "FastAPI" in content:
|
|
41
|
-
return "fastapi", 8000, f"uvicorn {module_name}:app --host 0.0.0.0 --port 8000"
|
|
42
|
-
|
|
43
|
-
if "Flask" in content:
|
|
44
|
-
return "flask", 5000, f"gunicorn {module_name}:app -b 0.0.0.0:5000"
|
|
45
|
-
|
|
46
|
-
return None, None, None
|
|
47
|
-
|
|
48
|
-
def generate_templated_assets(context: dict):
|
|
49
|
-
"""
|
|
50
|
-
Generates deployment assets (Dockerfile, docker-compose.yml) using Jinja2 templates.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
context: A dictionary containing information for rendering templates,
|
|
54
|
-
e.g., {'database': 'postgres', 'python_version': 'python:3.11-slim'}
|
|
55
|
-
"""
|
|
56
|
-
# Path to the templates directory
|
|
57
|
-
template_dir = Path(__file__).parent / "templates"
|
|
58
|
-
env = Environment(loader=FileSystemLoader(template_dir))
|
|
59
|
-
|
|
60
|
-
# Detect framework specifics
|
|
61
|
-
framework, port, command = detect_framework()
|
|
62
|
-
if not framework:
|
|
63
|
-
print("Warning: No recognizable web framework detected.")
|
|
64
|
-
return []
|
|
65
|
-
|
|
66
|
-
# Merge detected context with provided context
|
|
67
|
-
render_context = {
|
|
68
|
-
'port': port,
|
|
69
|
-
'command': command,
|
|
70
|
-
**context
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
generated_files = []
|
|
74
|
-
|
|
75
|
-
# --- 1. Dockerfile ---
|
|
76
|
-
dockerfile_template = env.get_template("Dockerfile.j2")
|
|
77
|
-
dockerfile_content = dockerfile_template.render(render_context)
|
|
78
|
-
with open("Dockerfile", "w") as f:
|
|
79
|
-
f.write(dockerfile_content)
|
|
80
|
-
generated_files.append("Dockerfile")
|
|
81
|
-
|
|
82
|
-
# --- 2. docker-compose.yml ---
|
|
83
|
-
compose_template = env.get_template("docker-compose.yml.j2")
|
|
84
|
-
compose_content = compose_template.render(render_context)
|
|
85
|
-
with open("docker-compose.yml", "w") as f:
|
|
86
|
-
f.write(compose_content)
|
|
87
|
-
generated_files.append("docker-compose.yml")
|
|
88
|
-
|
|
89
|
-
return generated_files
|