pactown 0.1.4__py3-none-any.whl → 0.1.47__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.
- pactown/__init__.py +178 -4
- pactown/cli.py +539 -37
- pactown/config.py +12 -11
- pactown/deploy/__init__.py +17 -3
- pactown/deploy/base.py +35 -33
- pactown/deploy/compose.py +59 -58
- pactown/deploy/docker.py +40 -41
- pactown/deploy/kubernetes.py +43 -42
- pactown/deploy/podman.py +55 -56
- pactown/deploy/quadlet.py +1021 -0
- pactown/deploy/quadlet_api.py +533 -0
- pactown/deploy/quadlet_shell.py +557 -0
- pactown/events.py +1066 -0
- pactown/fast_start.py +514 -0
- pactown/generator.py +31 -30
- pactown/llm.py +450 -0
- pactown/markpact_blocks.py +50 -0
- pactown/network.py +59 -38
- pactown/orchestrator.py +90 -93
- pactown/parallel.py +40 -40
- pactown/platform.py +146 -0
- pactown/registry/__init__.py +1 -1
- pactown/registry/client.py +45 -46
- pactown/registry/models.py +25 -25
- pactown/registry/server.py +24 -24
- pactown/resolver.py +30 -30
- pactown/runner_api.py +458 -0
- pactown/sandbox_manager.py +480 -79
- pactown/security.py +682 -0
- pactown/service_runner.py +1201 -0
- pactown/user_isolation.py +458 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/METADATA +65 -9
- pactown-0.1.47.dist-info/RECORD +36 -0
- pactown-0.1.47.dist-info/entry_points.txt +5 -0
- pactown-0.1.4.dist-info/RECORD +0 -24
- pactown-0.1.4.dist-info/entry_points.txt +0 -3
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/WHEEL +0 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""FastAPI endpoints for Quadlet deployment management.
|
|
2
|
+
|
|
3
|
+
Provides a REST API for generating and deploying Markdown services
|
|
4
|
+
using Podman Quadlet on VPS infrastructure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from fastapi import BackgroundTasks, FastAPI, HTTPException
|
|
14
|
+
from fastapi.responses import PlainTextResponse
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from .base import DeploymentConfig
|
|
18
|
+
from .quadlet import (
|
|
19
|
+
QuadletBackend,
|
|
20
|
+
QuadletConfig,
|
|
21
|
+
QuadletTemplates,
|
|
22
|
+
generate_markdown_service_quadlet,
|
|
23
|
+
generate_traefik_quadlet,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Pydantic models for API
|
|
28
|
+
class DeploymentRequest(BaseModel):
|
|
29
|
+
"""Request to deploy a Markdown service."""
|
|
30
|
+
|
|
31
|
+
markdown_content: str = Field(..., description="Markdown content to deploy")
|
|
32
|
+
name: Optional[str] = Field(None, description="Service name (auto-generated from content if not provided)")
|
|
33
|
+
tenant_id: str = Field("default", description="Tenant identifier")
|
|
34
|
+
subdomain: Optional[str] = Field(None, description="Subdomain for the service")
|
|
35
|
+
domain: str = Field("localhost", description="Base domain")
|
|
36
|
+
tls_enabled: bool = Field(False, description="Enable TLS/HTTPS")
|
|
37
|
+
|
|
38
|
+
# Resource limits
|
|
39
|
+
cpus: str = Field("0.5", description="CPU limit")
|
|
40
|
+
memory: str = Field("256M", description="Memory limit")
|
|
41
|
+
|
|
42
|
+
# Image
|
|
43
|
+
image: str = Field(
|
|
44
|
+
"ghcr.io/pactown/markdown-server:latest",
|
|
45
|
+
description="Container image for Markdown server"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ContainerRequest(BaseModel):
|
|
50
|
+
"""Request to generate a container Quadlet file."""
|
|
51
|
+
|
|
52
|
+
name: str = Field(..., description="Container name")
|
|
53
|
+
image: str = Field(..., description="Container image")
|
|
54
|
+
port: int = Field(..., description="Container port")
|
|
55
|
+
tenant_id: str = Field("default", description="Tenant identifier")
|
|
56
|
+
subdomain: Optional[str] = Field(None, description="Subdomain")
|
|
57
|
+
domain: str = Field("localhost", description="Base domain")
|
|
58
|
+
tls_enabled: bool = Field(False, description="Enable TLS")
|
|
59
|
+
|
|
60
|
+
# Environment variables
|
|
61
|
+
env: dict[str, str] = Field(default_factory=dict, description="Environment variables")
|
|
62
|
+
|
|
63
|
+
# Volumes
|
|
64
|
+
volumes: list[str] = Field(default_factory=list, description="Volume mounts")
|
|
65
|
+
|
|
66
|
+
# Health check
|
|
67
|
+
health_check: Optional[str] = Field(None, description="Health check endpoint")
|
|
68
|
+
|
|
69
|
+
# Resource limits
|
|
70
|
+
cpus: str = Field("0.5", description="CPU limit")
|
|
71
|
+
memory: str = Field("256M", description="Memory limit")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TraefikRequest(BaseModel):
|
|
75
|
+
"""Request to generate Traefik Quadlet files."""
|
|
76
|
+
|
|
77
|
+
domain: str = Field(..., description="Base domain for Traefik")
|
|
78
|
+
email: Optional[str] = Field(None, description="Email for Let's Encrypt")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DeploymentResponse(BaseModel):
|
|
82
|
+
"""Response from deployment operation."""
|
|
83
|
+
|
|
84
|
+
success: bool
|
|
85
|
+
service_name: str
|
|
86
|
+
message: str
|
|
87
|
+
url: Optional[str] = None
|
|
88
|
+
unit_files: list[str] = Field(default_factory=list)
|
|
89
|
+
error: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class QuadletFileResponse(BaseModel):
|
|
93
|
+
"""Response containing generated Quadlet files."""
|
|
94
|
+
|
|
95
|
+
files: list[dict[str, str]] # [{filename: content}]
|
|
96
|
+
tenant_path: str
|
|
97
|
+
instructions: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ServiceStatus(BaseModel):
|
|
101
|
+
"""Service status information."""
|
|
102
|
+
|
|
103
|
+
name: str
|
|
104
|
+
running: bool
|
|
105
|
+
state: str
|
|
106
|
+
pid: Optional[str] = None
|
|
107
|
+
tenant: str
|
|
108
|
+
url: Optional[str] = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ListServicesResponse(BaseModel):
|
|
112
|
+
"""Response listing all services."""
|
|
113
|
+
|
|
114
|
+
tenant_id: str
|
|
115
|
+
services: list[ServiceStatus]
|
|
116
|
+
total: int
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Create FastAPI app
|
|
120
|
+
def create_quadlet_api(
|
|
121
|
+
default_domain: str = "localhost",
|
|
122
|
+
default_tenant: str = "default",
|
|
123
|
+
user_mode: bool = True,
|
|
124
|
+
) -> FastAPI:
|
|
125
|
+
"""Create FastAPI application for Quadlet management."""
|
|
126
|
+
|
|
127
|
+
app = FastAPI(
|
|
128
|
+
title="Pactown Quadlet API",
|
|
129
|
+
description="Deploy Markdown services on VPS using Podman Quadlet",
|
|
130
|
+
version="1.0.0",
|
|
131
|
+
docs_url="/docs",
|
|
132
|
+
redoc_url="/redoc",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Dependency to get backend
|
|
136
|
+
def get_backend(
|
|
137
|
+
tenant_id: str = default_tenant,
|
|
138
|
+
domain: str = default_domain,
|
|
139
|
+
) -> QuadletBackend:
|
|
140
|
+
quadlet_config = QuadletConfig(
|
|
141
|
+
tenant_id=tenant_id,
|
|
142
|
+
domain=domain,
|
|
143
|
+
user_mode=user_mode,
|
|
144
|
+
)
|
|
145
|
+
deploy_config = DeploymentConfig.for_production()
|
|
146
|
+
return QuadletBackend(deploy_config, quadlet_config)
|
|
147
|
+
|
|
148
|
+
@app.get("/health")
|
|
149
|
+
async def health():
|
|
150
|
+
"""Health check endpoint."""
|
|
151
|
+
return {"status": "healthy", "service": "quadlet-api"}
|
|
152
|
+
|
|
153
|
+
@app.get("/version")
|
|
154
|
+
async def version():
|
|
155
|
+
"""Get Podman/Quadlet version."""
|
|
156
|
+
backend = get_backend()
|
|
157
|
+
podman_version = backend.get_quadlet_version()
|
|
158
|
+
return {
|
|
159
|
+
"api_version": "1.0.0",
|
|
160
|
+
"podman_version": podman_version,
|
|
161
|
+
"quadlet_available": backend.is_available(),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@app.post("/generate/markdown", response_model=QuadletFileResponse)
|
|
165
|
+
async def generate_markdown_quadlet(request: DeploymentRequest):
|
|
166
|
+
"""Generate Quadlet files for a Markdown service.
|
|
167
|
+
|
|
168
|
+
This endpoint generates Quadlet unit files that can be used
|
|
169
|
+
to deploy a Markdown file as a web service.
|
|
170
|
+
"""
|
|
171
|
+
# Create config
|
|
172
|
+
quadlet_config = QuadletConfig(
|
|
173
|
+
tenant_id=request.tenant_id,
|
|
174
|
+
domain=request.domain,
|
|
175
|
+
subdomain=request.subdomain,
|
|
176
|
+
tls_enabled=request.tls_enabled,
|
|
177
|
+
cpus=request.cpus,
|
|
178
|
+
memory=request.memory,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Generate service name from content if not provided
|
|
182
|
+
name = request.name
|
|
183
|
+
if not name:
|
|
184
|
+
# Generate name from first heading or hash
|
|
185
|
+
lines = request.markdown_content.strip().split("\n")
|
|
186
|
+
for line in lines:
|
|
187
|
+
if line.startswith("# "):
|
|
188
|
+
name = line[2:].strip().lower().replace(" ", "-").replace("_", "-")[:32]
|
|
189
|
+
break
|
|
190
|
+
if not name:
|
|
191
|
+
name = f"md-{hashlib.sha256(request.markdown_content.encode()).hexdigest()[:8]}"
|
|
192
|
+
|
|
193
|
+
# Create temporary markdown file
|
|
194
|
+
content_hash = hashlib.sha256(request.markdown_content.encode()).hexdigest()[:12]
|
|
195
|
+
markdown_path = Path(f"/tmp/pactown-markdown-{content_hash}.md")
|
|
196
|
+
markdown_path.write_text(request.markdown_content)
|
|
197
|
+
|
|
198
|
+
# Generate units
|
|
199
|
+
units = generate_markdown_service_quadlet(
|
|
200
|
+
markdown_path=markdown_path,
|
|
201
|
+
config=quadlet_config,
|
|
202
|
+
image=request.image,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
files = []
|
|
206
|
+
for unit in units:
|
|
207
|
+
files.append({
|
|
208
|
+
"filename": unit.filename,
|
|
209
|
+
"content": unit.content,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return QuadletFileResponse(
|
|
213
|
+
files=files,
|
|
214
|
+
tenant_path=str(quadlet_config.tenant_path),
|
|
215
|
+
instructions=f"""
|
|
216
|
+
To deploy this service:
|
|
217
|
+
|
|
218
|
+
1. Save the unit file(s) to: {quadlet_config.tenant_path}/
|
|
219
|
+
2. Run: systemctl --user daemon-reload
|
|
220
|
+
3. Run: systemctl --user enable --now {name}.service
|
|
221
|
+
4. Access at: {"https" if request.tls_enabled else "http"}://{quadlet_config.full_domain}
|
|
222
|
+
|
|
223
|
+
Or use the /deploy/markdown endpoint to deploy automatically.
|
|
224
|
+
""".strip(),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@app.post("/generate/container", response_model=QuadletFileResponse)
|
|
228
|
+
async def generate_container_quadlet(request: ContainerRequest):
|
|
229
|
+
"""Generate Quadlet files for a custom container."""
|
|
230
|
+
quadlet_config = QuadletConfig(
|
|
231
|
+
tenant_id=request.tenant_id,
|
|
232
|
+
domain=request.domain,
|
|
233
|
+
subdomain=request.subdomain,
|
|
234
|
+
tls_enabled=request.tls_enabled,
|
|
235
|
+
cpus=request.cpus,
|
|
236
|
+
memory=request.memory,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
unit = QuadletTemplates.container(
|
|
240
|
+
name=request.name,
|
|
241
|
+
image=request.image,
|
|
242
|
+
port=request.port,
|
|
243
|
+
config=quadlet_config,
|
|
244
|
+
env=request.env,
|
|
245
|
+
health_check=request.health_check,
|
|
246
|
+
volumes=request.volumes,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return QuadletFileResponse(
|
|
250
|
+
files=[{
|
|
251
|
+
"filename": unit.filename,
|
|
252
|
+
"content": unit.content,
|
|
253
|
+
}],
|
|
254
|
+
tenant_path=str(quadlet_config.tenant_path),
|
|
255
|
+
instructions=f"""
|
|
256
|
+
To deploy this container:
|
|
257
|
+
|
|
258
|
+
1. Save the unit file to: {quadlet_config.tenant_path}/{unit.filename}
|
|
259
|
+
2. Run: systemctl --user daemon-reload
|
|
260
|
+
3. Run: systemctl --user enable --now {request.name}.service
|
|
261
|
+
4. Access at: {"https" if request.tls_enabled else "http"}://{quadlet_config.full_domain}
|
|
262
|
+
""".strip(),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@app.post("/generate/traefik", response_model=QuadletFileResponse)
|
|
266
|
+
async def generate_traefik_files(request: TraefikRequest):
|
|
267
|
+
"""Generate Traefik reverse proxy Quadlet files."""
|
|
268
|
+
quadlet_config = QuadletConfig(
|
|
269
|
+
domain=request.domain,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
units = generate_traefik_quadlet(quadlet_config)
|
|
273
|
+
|
|
274
|
+
files = []
|
|
275
|
+
for unit in units:
|
|
276
|
+
content = unit.content
|
|
277
|
+
if request.email:
|
|
278
|
+
content = content.replace(
|
|
279
|
+
f"admin@{request.domain}",
|
|
280
|
+
request.email
|
|
281
|
+
)
|
|
282
|
+
files.append({
|
|
283
|
+
"filename": unit.filename,
|
|
284
|
+
"content": content,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
return QuadletFileResponse(
|
|
288
|
+
files=files,
|
|
289
|
+
tenant_path=str(quadlet_config.systemd_path),
|
|
290
|
+
instructions=f"""
|
|
291
|
+
To deploy Traefik:
|
|
292
|
+
|
|
293
|
+
1. Save the unit files to: {quadlet_config.systemd_path}/
|
|
294
|
+
2. Run: systemctl --user daemon-reload
|
|
295
|
+
3. Run: systemctl --user enable --now traefik.service
|
|
296
|
+
|
|
297
|
+
Traefik will automatically:
|
|
298
|
+
- Handle HTTP to HTTPS redirect
|
|
299
|
+
- Provision Let's Encrypt certificates
|
|
300
|
+
- Route traffic to your services based on Host labels
|
|
301
|
+
""".strip(),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
@app.post("/deploy/markdown", response_model=DeploymentResponse)
|
|
305
|
+
async def deploy_markdown(request: DeploymentRequest, background_tasks: BackgroundTasks):
|
|
306
|
+
"""Deploy a Markdown file as a web service.
|
|
307
|
+
|
|
308
|
+
This endpoint generates Quadlet files and starts the service.
|
|
309
|
+
"""
|
|
310
|
+
quadlet_config = QuadletConfig(
|
|
311
|
+
tenant_id=request.tenant_id,
|
|
312
|
+
domain=request.domain,
|
|
313
|
+
subdomain=request.subdomain,
|
|
314
|
+
tls_enabled=request.tls_enabled,
|
|
315
|
+
cpus=request.cpus,
|
|
316
|
+
memory=request.memory,
|
|
317
|
+
user_mode=user_mode,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
deploy_config = DeploymentConfig.for_production()
|
|
321
|
+
backend = QuadletBackend(deploy_config, quadlet_config)
|
|
322
|
+
|
|
323
|
+
if not backend.is_available():
|
|
324
|
+
raise HTTPException(
|
|
325
|
+
status_code=503,
|
|
326
|
+
detail="Podman with Quadlet support not available"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Generate name
|
|
330
|
+
name = request.name
|
|
331
|
+
if not name:
|
|
332
|
+
lines = request.markdown_content.strip().split("\n")
|
|
333
|
+
for line in lines:
|
|
334
|
+
if line.startswith("# "):
|
|
335
|
+
name = line[2:].strip().lower().replace(" ", "-").replace("_", "-")[:32]
|
|
336
|
+
break
|
|
337
|
+
if not name:
|
|
338
|
+
name = f"md-{hashlib.sha256(request.markdown_content.encode()).hexdigest()[:8]}"
|
|
339
|
+
|
|
340
|
+
# Save markdown content
|
|
341
|
+
content_dir = quadlet_config.tenant_path / "content"
|
|
342
|
+
content_dir.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
markdown_path = content_dir / f"{name}.md"
|
|
344
|
+
markdown_path.write_text(request.markdown_content)
|
|
345
|
+
|
|
346
|
+
# Generate and save units
|
|
347
|
+
units = generate_markdown_service_quadlet(
|
|
348
|
+
markdown_path=markdown_path,
|
|
349
|
+
config=quadlet_config,
|
|
350
|
+
image=request.image,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
unit_files = []
|
|
354
|
+
for unit in units:
|
|
355
|
+
path = unit.save(quadlet_config.tenant_path)
|
|
356
|
+
unit_files.append(str(path))
|
|
357
|
+
|
|
358
|
+
# Reload and start
|
|
359
|
+
backend._systemctl("daemon-reload")
|
|
360
|
+
backend._systemctl("enable", f"{name}.service")
|
|
361
|
+
result = backend._systemctl("start", f"{name}.service")
|
|
362
|
+
|
|
363
|
+
url = f"{'https' if request.tls_enabled else 'http'}://{quadlet_config.full_domain}"
|
|
364
|
+
|
|
365
|
+
if result.returncode == 0:
|
|
366
|
+
return DeploymentResponse(
|
|
367
|
+
success=True,
|
|
368
|
+
service_name=name,
|
|
369
|
+
message=f"Successfully deployed {name}",
|
|
370
|
+
url=url,
|
|
371
|
+
unit_files=unit_files,
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
return DeploymentResponse(
|
|
375
|
+
success=False,
|
|
376
|
+
service_name=name,
|
|
377
|
+
message="Deployment failed",
|
|
378
|
+
error=result.stderr,
|
|
379
|
+
unit_files=unit_files,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
@app.delete("/deploy/{service_name}", response_model=DeploymentResponse)
|
|
383
|
+
async def undeploy_service(
|
|
384
|
+
service_name: str,
|
|
385
|
+
tenant_id: str = default_tenant,
|
|
386
|
+
):
|
|
387
|
+
"""Remove a deployed service."""
|
|
388
|
+
backend = get_backend(tenant_id=tenant_id)
|
|
389
|
+
result = backend.stop(service_name)
|
|
390
|
+
|
|
391
|
+
return DeploymentResponse(
|
|
392
|
+
success=result.success,
|
|
393
|
+
service_name=service_name,
|
|
394
|
+
message="Service removed" if result.success else "Failed to remove service",
|
|
395
|
+
error=result.error,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
@app.get("/services", response_model=ListServicesResponse)
|
|
399
|
+
async def list_services(tenant_id: str = default_tenant):
|
|
400
|
+
"""List all services for a tenant."""
|
|
401
|
+
backend = get_backend(tenant_id=tenant_id)
|
|
402
|
+
services = backend.list_services()
|
|
403
|
+
|
|
404
|
+
service_statuses = []
|
|
405
|
+
for svc in services:
|
|
406
|
+
status = svc["status"]
|
|
407
|
+
service_statuses.append(ServiceStatus(
|
|
408
|
+
name=svc["name"],
|
|
409
|
+
running=status.get("running", False),
|
|
410
|
+
state=status.get("state", "unknown"),
|
|
411
|
+
pid=status.get("pid"),
|
|
412
|
+
tenant=tenant_id,
|
|
413
|
+
))
|
|
414
|
+
|
|
415
|
+
return ListServicesResponse(
|
|
416
|
+
tenant_id=tenant_id,
|
|
417
|
+
services=service_statuses,
|
|
418
|
+
total=len(service_statuses),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
@app.get("/services/{service_name}", response_model=ServiceStatus)
|
|
422
|
+
async def get_service_status(
|
|
423
|
+
service_name: str,
|
|
424
|
+
tenant_id: str = default_tenant,
|
|
425
|
+
):
|
|
426
|
+
"""Get status of a specific service."""
|
|
427
|
+
backend = get_backend(tenant_id=tenant_id)
|
|
428
|
+
status = backend.status(service_name)
|
|
429
|
+
|
|
430
|
+
return ServiceStatus(
|
|
431
|
+
name=service_name,
|
|
432
|
+
running=status.get("running", False),
|
|
433
|
+
state=status.get("state", "unknown"),
|
|
434
|
+
pid=status.get("pid"),
|
|
435
|
+
tenant=tenant_id,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
@app.post("/services/{service_name}/start")
|
|
439
|
+
async def start_service(
|
|
440
|
+
service_name: str,
|
|
441
|
+
tenant_id: str = default_tenant,
|
|
442
|
+
):
|
|
443
|
+
"""Start a service."""
|
|
444
|
+
backend = get_backend(tenant_id=tenant_id)
|
|
445
|
+
backend._systemctl("daemon-reload")
|
|
446
|
+
result = backend._systemctl("start", f"{service_name}.service")
|
|
447
|
+
|
|
448
|
+
if result.returncode == 0:
|
|
449
|
+
return {"success": True, "message": f"Started {service_name}"}
|
|
450
|
+
else:
|
|
451
|
+
raise HTTPException(status_code=500, detail=result.stderr)
|
|
452
|
+
|
|
453
|
+
@app.post("/services/{service_name}/stop")
|
|
454
|
+
async def stop_service(
|
|
455
|
+
service_name: str,
|
|
456
|
+
tenant_id: str = default_tenant,
|
|
457
|
+
):
|
|
458
|
+
"""Stop a service."""
|
|
459
|
+
backend = get_backend(tenant_id=tenant_id)
|
|
460
|
+
result = backend._systemctl("stop", f"{service_name}.service")
|
|
461
|
+
|
|
462
|
+
if result.returncode == 0:
|
|
463
|
+
return {"success": True, "message": f"Stopped {service_name}"}
|
|
464
|
+
else:
|
|
465
|
+
raise HTTPException(status_code=500, detail=result.stderr)
|
|
466
|
+
|
|
467
|
+
@app.post("/services/{service_name}/restart")
|
|
468
|
+
async def restart_service(
|
|
469
|
+
service_name: str,
|
|
470
|
+
tenant_id: str = default_tenant,
|
|
471
|
+
):
|
|
472
|
+
"""Restart a service."""
|
|
473
|
+
backend = get_backend(tenant_id=tenant_id)
|
|
474
|
+
result = backend._systemctl("restart", f"{service_name}.service")
|
|
475
|
+
|
|
476
|
+
if result.returncode == 0:
|
|
477
|
+
return {"success": True, "message": f"Restarted {service_name}"}
|
|
478
|
+
else:
|
|
479
|
+
raise HTTPException(status_code=500, detail=result.stderr)
|
|
480
|
+
|
|
481
|
+
@app.get("/services/{service_name}/logs", response_class=PlainTextResponse)
|
|
482
|
+
async def get_service_logs(
|
|
483
|
+
service_name: str,
|
|
484
|
+
tenant_id: str = default_tenant,
|
|
485
|
+
lines: int = 100,
|
|
486
|
+
):
|
|
487
|
+
"""Get logs for a service."""
|
|
488
|
+
backend = get_backend(tenant_id=tenant_id)
|
|
489
|
+
logs = backend.logs(service_name, tail=lines)
|
|
490
|
+
return logs
|
|
491
|
+
|
|
492
|
+
@app.get("/unit/{service_name}", response_class=PlainTextResponse)
|
|
493
|
+
async def get_unit_file(
|
|
494
|
+
service_name: str,
|
|
495
|
+
tenant_id: str = default_tenant,
|
|
496
|
+
):
|
|
497
|
+
"""Get the Quadlet unit file content for a service."""
|
|
498
|
+
quadlet_config = QuadletConfig(tenant_id=tenant_id, user_mode=user_mode)
|
|
499
|
+
|
|
500
|
+
for ext in ["container", "pod", "network", "volume", "kube"]:
|
|
501
|
+
unit_path = quadlet_config.tenant_path / f"{service_name}.{ext}"
|
|
502
|
+
if unit_path.exists():
|
|
503
|
+
return unit_path.read_text()
|
|
504
|
+
|
|
505
|
+
raise HTTPException(status_code=404, detail="Unit file not found")
|
|
506
|
+
|
|
507
|
+
return app
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# Default app instance
|
|
511
|
+
app = create_quadlet_api()
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def run_api(
|
|
515
|
+
host: str = "0.0.0.0",
|
|
516
|
+
port: int = 8800,
|
|
517
|
+
domain: str = "localhost",
|
|
518
|
+
tenant: str = "default",
|
|
519
|
+
):
|
|
520
|
+
"""Run the Quadlet API server."""
|
|
521
|
+
import uvicorn
|
|
522
|
+
|
|
523
|
+
global app
|
|
524
|
+
app = create_quadlet_api(
|
|
525
|
+
default_domain=domain,
|
|
526
|
+
default_tenant=tenant,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
uvicorn.run(app, host=host, port=port)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
if __name__ == "__main__":
|
|
533
|
+
run_api()
|