runtm-api 0.1.0__tar.gz
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.
- runtm_api-0.1.0/.gitignore +84 -0
- runtm_api-0.1.0/LICENSE +37 -0
- runtm_api-0.1.0/PKG-INFO +126 -0
- runtm_api-0.1.0/README.md +81 -0
- runtm_api-0.1.0/alembic/env.py +86 -0
- runtm_api-0.1.0/alembic/script.py.mako +28 -0
- runtm_api-0.1.0/alembic/versions/20251215_0001_initial_schema.py +176 -0
- runtm_api-0.1.0/alembic/versions/20251228_0002_telemetry_tables.py +163 -0
- runtm_api-0.1.0/alembic/versions/20251229_0003_discovery_json.py +46 -0
- runtm_api-0.1.0/alembic/versions/20251230_0004_multi_tenant.py +179 -0
- runtm_api-0.1.0/alembic/versions/20251231_0005_deploy_optimization.py +66 -0
- runtm_api-0.1.0/alembic/versions/20260101_0006_deployment_expiry.py +77 -0
- runtm_api-0.1.0/alembic/versions/20260102_0007_add_ready_at.py +47 -0
- runtm_api-0.1.0/alembic.ini +112 -0
- runtm_api-0.1.0/pyproject.toml +64 -0
- runtm_api-0.1.0/runtm_api/__init__.py +3 -0
- runtm_api-0.1.0/runtm_api/auth/__init__.py +29 -0
- runtm_api-0.1.0/runtm_api/auth/keys.py +126 -0
- runtm_api-0.1.0/runtm_api/auth/token.py +486 -0
- runtm_api-0.1.0/runtm_api/core/__init__.py +1 -0
- runtm_api-0.1.0/runtm_api/core/config.py +277 -0
- runtm_api-0.1.0/runtm_api/db/__init__.py +44 -0
- runtm_api-0.1.0/runtm_api/db/models.py +604 -0
- runtm_api-0.1.0/runtm_api/db/repository.py +281 -0
- runtm_api-0.1.0/runtm_api/db/session.py +82 -0
- runtm_api-0.1.0/runtm_api/main.py +118 -0
- runtm_api-0.1.0/runtm_api/middleware/__init__.py +17 -0
- runtm_api-0.1.0/runtm_api/middleware/proxy.py +207 -0
- runtm_api-0.1.0/runtm_api/routes/__init__.py +13 -0
- runtm_api-0.1.0/runtm_api/routes/deployments.py +1528 -0
- runtm_api-0.1.0/runtm_api/routes/health.py +29 -0
- runtm_api-0.1.0/runtm_api/routes/me.py +90 -0
- runtm_api-0.1.0/runtm_api/routes/telemetry.py +379 -0
- runtm_api-0.1.0/runtm_api/services/__init__.py +23 -0
- runtm_api-0.1.0/runtm_api/services/idempotency.py +97 -0
- runtm_api-0.1.0/runtm_api/services/policy.py +308 -0
- runtm_api-0.1.0/runtm_api/services/queue.py +73 -0
- runtm_api-0.1.0/runtm_api/services/rate_limit.py +325 -0
- runtm_api-0.1.0/runtm_api/services/telemetry.py +640 -0
- runtm_api-0.1.0/runtm_api/services/usage.py +221 -0
- runtm_api-0.1.0/runtm_api/telemetry.py +144 -0
- runtm_api-0.1.0/tests/__init__.py +1 -0
- runtm_api-0.1.0/tests/test_auth.py +29 -0
- runtm_api-0.1.0/tests/test_health.py +26 -0
- runtm_api-0.1.0/tests/test_multi_tenant.py +395 -0
- runtm_api-0.1.0/tests/test_policy.py +311 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
/build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Environments
|
|
54
|
+
.env
|
|
55
|
+
.env.local
|
|
56
|
+
.env.*
|
|
57
|
+
.venv
|
|
58
|
+
env/
|
|
59
|
+
venv/
|
|
60
|
+
ENV/
|
|
61
|
+
env.bak/
|
|
62
|
+
venv.bak/
|
|
63
|
+
|
|
64
|
+
# IDE
|
|
65
|
+
.idea/
|
|
66
|
+
*.swp
|
|
67
|
+
*.swo
|
|
68
|
+
*~
|
|
69
|
+
.cursor/
|
|
70
|
+
.cursor/rules/
|
|
71
|
+
|
|
72
|
+
# macOS
|
|
73
|
+
.DS_Store
|
|
74
|
+
|
|
75
|
+
# Project specific
|
|
76
|
+
/artifacts/
|
|
77
|
+
*.log
|
|
78
|
+
|
|
79
|
+
# Internal planning docs
|
|
80
|
+
.plans/
|
|
81
|
+
|
|
82
|
+
# Alembic
|
|
83
|
+
packages/api/alembic/versions/*.pyc
|
|
84
|
+
|
runtm_api-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# AGPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Runtm
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
(at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Full License Text
|
|
21
|
+
|
|
22
|
+
The full text of the GNU Affero General Public License v3.0 is available at:
|
|
23
|
+
https://www.gnu.org/licenses/agpl-3.0.txt
|
|
24
|
+
|
|
25
|
+
### Summary
|
|
26
|
+
|
|
27
|
+
The AGPL-3.0 license requires that:
|
|
28
|
+
|
|
29
|
+
1. **Source Code Access**: If you run a modified version of this software as a network service, you must make the complete source code available to users of that service.
|
|
30
|
+
|
|
31
|
+
2. **Copyleft**: Any modifications must also be licensed under AGPL-3.0.
|
|
32
|
+
|
|
33
|
+
3. **Attribution**: You must retain copyright notices and license information.
|
|
34
|
+
|
|
35
|
+
4. **State Changes**: You must document any changes made to the source code.
|
|
36
|
+
|
|
37
|
+
This license ensures that improvements to hosted versions of Runtm remain open source and benefit the community.
|
runtm_api-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runtm-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastAPI control plane for Runtm
|
|
5
|
+
Project-URL: Homepage, https://runtm.com
|
|
6
|
+
Project-URL: Documentation, https://docs.runtm.com
|
|
7
|
+
Project-URL: Repository, https://github.com/runtm-ai/runtm
|
|
8
|
+
Project-URL: Issues, https://github.com/runtm-ai/runtm/issues
|
|
9
|
+
Author-email: Gustavo Trigos <gus@runtm.com>
|
|
10
|
+
Maintainer-email: Gustavo Trigos <gus@runtm.com>
|
|
11
|
+
License: AGPL-3.0-or-later
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,control-plane,deployment,fastapi
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: alembic<2.0,>=1.10
|
|
27
|
+
Requires-Dist: asyncpg<1.0,>=0.27
|
|
28
|
+
Requires-Dist: fastapi<1.0,>=0.100.0
|
|
29
|
+
Requires-Dist: psycopg2-binary<3.0,>=2.9
|
|
30
|
+
Requires-Dist: pydantic-settings<3.0,>=2.0
|
|
31
|
+
Requires-Dist: python-multipart<1.0,>=0.0.6
|
|
32
|
+
Requires-Dist: redis<6.0,>=4.5
|
|
33
|
+
Requires-Dist: rq<3.0,>=2.3.2
|
|
34
|
+
Requires-Dist: runtm-shared>=0.1.0
|
|
35
|
+
Requires-Dist: sqlalchemy<3.0,>=2.0
|
|
36
|
+
Requires-Dist: uvicorn[standard]<1.0,>=0.20.0
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: httpx<1.0,>=0.24; extra == 'dev'
|
|
39
|
+
Requires-Dist: mypy<2.0,>=1.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-asyncio<1.0,>=0.21; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest-cov<6.0,>=4.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: pytest<9.0,>=7.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# runtm-api
|
|
47
|
+
|
|
48
|
+
FastAPI control plane for Runtm. See the [API docs](https://docs.runtm.com/api/overview) for endpoint reference.
|
|
49
|
+
|
|
50
|
+
**Hosted API:** [app.runtm.com](https://app.runtm.com)
|
|
51
|
+
|
|
52
|
+
## Endpoints
|
|
53
|
+
|
|
54
|
+
| Method | Path | Description |
|
|
55
|
+
|--------|------|-------------|
|
|
56
|
+
| POST | `/v0/deployments` | Create deployment (multipart: manifest + artifact.zip) |
|
|
57
|
+
| GET | `/v0/deployments/:id` | Get deployment status, URL, timestamps, error |
|
|
58
|
+
| GET | `/v0/deployments/:id/logs` | Get build/deploy logs |
|
|
59
|
+
|
|
60
|
+
### Creating Deployments
|
|
61
|
+
|
|
62
|
+
**POST `/v0/deployments`**
|
|
63
|
+
|
|
64
|
+
Creates a new deployment or redeploys an existing one (based on manifest name).
|
|
65
|
+
|
|
66
|
+
**Query Parameters:**
|
|
67
|
+
- `new=true` - Force creation of new deployment instead of redeploying existing
|
|
68
|
+
- `tier=starter|standard|performance` - Override machine tier from manifest
|
|
69
|
+
|
|
70
|
+
**Request:**
|
|
71
|
+
- `manifest` (file) - `runtm.yaml` manifest file
|
|
72
|
+
- `artifact` (file) - `artifact.zip` containing project files
|
|
73
|
+
|
|
74
|
+
**Headers:**
|
|
75
|
+
- `Authorization: Bearer <token>` - API authentication token
|
|
76
|
+
- `Idempotency-Key: <key>` - Optional idempotency key for retries
|
|
77
|
+
|
|
78
|
+
**Example:**
|
|
79
|
+
```bash
|
|
80
|
+
curl -X POST "http://localhost:8000/v0/deployments?tier=performance" \
|
|
81
|
+
-H "Authorization: Bearer $RUNTM_API_KEY" \
|
|
82
|
+
-F "manifest=@runtm.yaml" \
|
|
83
|
+
-F "artifact=@artifact.zip"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Machine Tiers:**
|
|
87
|
+
|
|
88
|
+
All deployments use **auto-stop** for cost savings (machines stop when idle and start automatically on traffic).
|
|
89
|
+
|
|
90
|
+
| Tier | CPUs | Memory | Est. Cost |
|
|
91
|
+
|------|------|--------|-----------|
|
|
92
|
+
| `starter` (default) | 1 shared | 256MB | ~$2/month* |
|
|
93
|
+
| `standard` | 1 shared | 512MB | ~$5/month* |
|
|
94
|
+
| `performance` | 2 shared | 1GB | ~$10/month* |
|
|
95
|
+
|
|
96
|
+
*Costs are estimates for 24/7 operation. With auto-stop, costs are much lower for low-traffic services.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Install dependencies
|
|
102
|
+
pip install -e ".[dev]"
|
|
103
|
+
|
|
104
|
+
# Run migrations
|
|
105
|
+
alembic upgrade head
|
|
106
|
+
|
|
107
|
+
# Start server
|
|
108
|
+
uvicorn runtm_api.main:app --reload --host 0.0.0.0 --port 8000
|
|
109
|
+
|
|
110
|
+
# Run tests
|
|
111
|
+
pytest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Environment Variables
|
|
115
|
+
|
|
116
|
+
| Variable | Description | Default |
|
|
117
|
+
|----------|-------------|---------|
|
|
118
|
+
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
|
119
|
+
| `REDIS_URL` | Redis connection string | Required |
|
|
120
|
+
| `RUNTM_API_SECRET` | API authentication secret (what server validates against) | Required |
|
|
121
|
+
| `ARTIFACT_STORAGE_PATH` | Local artifact storage path | `/artifacts` |
|
|
122
|
+
| `ARTIFACT_STORAGE_BACKEND` | Storage backend: `local` or `s3` | `local` |
|
|
123
|
+
| `BUCKET_NAME` | S3/Tigris bucket name (when backend=s3) | - |
|
|
124
|
+
| `AWS_ENDPOINT_URL_S3` | S3 endpoint URL (e.g. `https://fly.storage.tigris.dev`) | - |
|
|
125
|
+
| `AUTH_MODE` | Auth mode: `single_tenant` or `multi_tenant` | `single_tenant` |
|
|
126
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# runtm-api
|
|
2
|
+
|
|
3
|
+
FastAPI control plane for Runtm. See the [API docs](https://docs.runtm.com/api/overview) for endpoint reference.
|
|
4
|
+
|
|
5
|
+
**Hosted API:** [app.runtm.com](https://app.runtm.com)
|
|
6
|
+
|
|
7
|
+
## Endpoints
|
|
8
|
+
|
|
9
|
+
| Method | Path | Description |
|
|
10
|
+
|--------|------|-------------|
|
|
11
|
+
| POST | `/v0/deployments` | Create deployment (multipart: manifest + artifact.zip) |
|
|
12
|
+
| GET | `/v0/deployments/:id` | Get deployment status, URL, timestamps, error |
|
|
13
|
+
| GET | `/v0/deployments/:id/logs` | Get build/deploy logs |
|
|
14
|
+
|
|
15
|
+
### Creating Deployments
|
|
16
|
+
|
|
17
|
+
**POST `/v0/deployments`**
|
|
18
|
+
|
|
19
|
+
Creates a new deployment or redeploys an existing one (based on manifest name).
|
|
20
|
+
|
|
21
|
+
**Query Parameters:**
|
|
22
|
+
- `new=true` - Force creation of new deployment instead of redeploying existing
|
|
23
|
+
- `tier=starter|standard|performance` - Override machine tier from manifest
|
|
24
|
+
|
|
25
|
+
**Request:**
|
|
26
|
+
- `manifest` (file) - `runtm.yaml` manifest file
|
|
27
|
+
- `artifact` (file) - `artifact.zip` containing project files
|
|
28
|
+
|
|
29
|
+
**Headers:**
|
|
30
|
+
- `Authorization: Bearer <token>` - API authentication token
|
|
31
|
+
- `Idempotency-Key: <key>` - Optional idempotency key for retries
|
|
32
|
+
|
|
33
|
+
**Example:**
|
|
34
|
+
```bash
|
|
35
|
+
curl -X POST "http://localhost:8000/v0/deployments?tier=performance" \
|
|
36
|
+
-H "Authorization: Bearer $RUNTM_API_KEY" \
|
|
37
|
+
-F "manifest=@runtm.yaml" \
|
|
38
|
+
-F "artifact=@artifact.zip"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Machine Tiers:**
|
|
42
|
+
|
|
43
|
+
All deployments use **auto-stop** for cost savings (machines stop when idle and start automatically on traffic).
|
|
44
|
+
|
|
45
|
+
| Tier | CPUs | Memory | Est. Cost |
|
|
46
|
+
|------|------|--------|-----------|
|
|
47
|
+
| `starter` (default) | 1 shared | 256MB | ~$2/month* |
|
|
48
|
+
| `standard` | 1 shared | 512MB | ~$5/month* |
|
|
49
|
+
| `performance` | 2 shared | 1GB | ~$10/month* |
|
|
50
|
+
|
|
51
|
+
*Costs are estimates for 24/7 operation. With auto-stop, costs are much lower for low-traffic services.
|
|
52
|
+
|
|
53
|
+
## Development
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Install dependencies
|
|
57
|
+
pip install -e ".[dev]"
|
|
58
|
+
|
|
59
|
+
# Run migrations
|
|
60
|
+
alembic upgrade head
|
|
61
|
+
|
|
62
|
+
# Start server
|
|
63
|
+
uvicorn runtm_api.main:app --reload --host 0.0.0.0 --port 8000
|
|
64
|
+
|
|
65
|
+
# Run tests
|
|
66
|
+
pytest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Environment Variables
|
|
70
|
+
|
|
71
|
+
| Variable | Description | Default |
|
|
72
|
+
|----------|-------------|---------|
|
|
73
|
+
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
|
74
|
+
| `REDIS_URL` | Redis connection string | Required |
|
|
75
|
+
| `RUNTM_API_SECRET` | API authentication secret (what server validates against) | Required |
|
|
76
|
+
| `ARTIFACT_STORAGE_PATH` | Local artifact storage path | `/artifacts` |
|
|
77
|
+
| `ARTIFACT_STORAGE_BACKEND` | Storage backend: `local` or `s3` | `local` |
|
|
78
|
+
| `BUCKET_NAME` | S3/Tigris bucket name (when backend=s3) | - |
|
|
79
|
+
| `AWS_ENDPOINT_URL_S3` | S3 endpoint URL (e.g. `https://fly.storage.tigris.dev`) | - |
|
|
80
|
+
| `AUTH_MODE` | Auth mode: `single_tenant` or `multi_tenant` | `single_tenant` |
|
|
81
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Alembic environment configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from logging.config import fileConfig
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import engine_from_config, pool
|
|
9
|
+
|
|
10
|
+
from alembic import context
|
|
11
|
+
|
|
12
|
+
# Import Base and all models for autogenerate support
|
|
13
|
+
from runtm_api.db.models import Base
|
|
14
|
+
|
|
15
|
+
# this is the Alembic Config object, which provides
|
|
16
|
+
# access to the values within the .ini file in use.
|
|
17
|
+
config = context.config
|
|
18
|
+
|
|
19
|
+
# Interpret the config file for Python logging.
|
|
20
|
+
if config.config_file_name is not None:
|
|
21
|
+
fileConfig(config.config_file_name)
|
|
22
|
+
|
|
23
|
+
# Model metadata for 'autogenerate' support
|
|
24
|
+
target_metadata = Base.metadata
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_url() -> str:
|
|
28
|
+
"""Get database URL from environment or config."""
|
|
29
|
+
return os.environ.get(
|
|
30
|
+
"DATABASE_URL",
|
|
31
|
+
config.get_main_option("sqlalchemy.url", ""),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_migrations_offline() -> None:
|
|
36
|
+
"""Run migrations in 'offline' mode.
|
|
37
|
+
|
|
38
|
+
This configures the context with just a URL
|
|
39
|
+
and not an Engine, though an Engine is acceptable
|
|
40
|
+
here as well. By skipping the Engine creation
|
|
41
|
+
we don't even need a DBAPI to be available.
|
|
42
|
+
|
|
43
|
+
Calls to context.execute() here emit the given string to the
|
|
44
|
+
script output.
|
|
45
|
+
"""
|
|
46
|
+
url = get_url()
|
|
47
|
+
context.configure(
|
|
48
|
+
url=url,
|
|
49
|
+
target_metadata=target_metadata,
|
|
50
|
+
literal_binds=True,
|
|
51
|
+
dialect_opts={"paramstyle": "named"},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
with context.begin_transaction():
|
|
55
|
+
context.run_migrations()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_migrations_online() -> None:
|
|
59
|
+
"""Run migrations in 'online' mode.
|
|
60
|
+
|
|
61
|
+
In this scenario we need to create an Engine
|
|
62
|
+
and associate a connection with the context.
|
|
63
|
+
"""
|
|
64
|
+
configuration = config.get_section(config.config_ini_section) or {}
|
|
65
|
+
configuration["sqlalchemy.url"] = get_url()
|
|
66
|
+
|
|
67
|
+
connectable = engine_from_config(
|
|
68
|
+
configuration,
|
|
69
|
+
prefix="sqlalchemy.",
|
|
70
|
+
poolclass=pool.NullPool,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with connectable.connect() as connection:
|
|
74
|
+
context.configure(
|
|
75
|
+
connection=connection,
|
|
76
|
+
target_metadata=target_metadata,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
with context.begin_transaction():
|
|
80
|
+
context.run_migrations()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if context.is_offline_mode():
|
|
84
|
+
run_migrations_offline()
|
|
85
|
+
else:
|
|
86
|
+
run_migrations_online()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
${imports if imports else ""}
|
|
14
|
+
|
|
15
|
+
# revision identifiers, used by Alembic.
|
|
16
|
+
revision: str = ${repr(up_revision)}
|
|
17
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
${upgrades if upgrades else "pass"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
${downgrades if downgrades else "pass"}
|
|
28
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Initial schema: deployments, provider_resources, idempotency_keys, build_logs
|
|
2
|
+
|
|
3
|
+
Revision ID: 0001
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2025-12-15
|
|
6
|
+
|
|
7
|
+
Full database schema for Runtm including:
|
|
8
|
+
- deployments: Core deployment records with version tracking for redeployments
|
|
9
|
+
- provider_resources: Fly.io/Cloud Run resource mappings
|
|
10
|
+
- idempotency_keys: Safe retry support
|
|
11
|
+
- build_logs: Build and deploy log storage
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
|
|
18
|
+
import sqlalchemy as sa
|
|
19
|
+
from sqlalchemy.dialects import postgresql
|
|
20
|
+
|
|
21
|
+
from alembic import op
|
|
22
|
+
|
|
23
|
+
# revision identifiers, used by Alembic.
|
|
24
|
+
revision: str = "0001"
|
|
25
|
+
down_revision: str | None = None
|
|
26
|
+
branch_labels: str | Sequence[str] | None = None
|
|
27
|
+
depends_on: str | Sequence[str] | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def upgrade() -> None:
|
|
31
|
+
# =========================================================================
|
|
32
|
+
# Deployments table
|
|
33
|
+
# =========================================================================
|
|
34
|
+
# Core deployment records. Each deployment represents a single version
|
|
35
|
+
# of a project deployed to a URL. Redeployments create new records
|
|
36
|
+
# linked via previous_deployment_id.
|
|
37
|
+
op.create_table(
|
|
38
|
+
"deployments",
|
|
39
|
+
# Primary key (internal UUID)
|
|
40
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
41
|
+
# Human-friendly deployment ID (e.g., dep_abc123def456)
|
|
42
|
+
sa.Column("deployment_id", sa.String(32), unique=True, nullable=False),
|
|
43
|
+
# Multi-tenant fields (nullable for single-tenant mode)
|
|
44
|
+
sa.Column("owner_id", sa.String(64), nullable=True),
|
|
45
|
+
sa.Column("api_key_id", sa.String(64), nullable=True),
|
|
46
|
+
# Deployment metadata
|
|
47
|
+
sa.Column("name", sa.String(63), nullable=False),
|
|
48
|
+
sa.Column("state", sa.String(32), nullable=False, default="queued"),
|
|
49
|
+
sa.Column("artifact_key", sa.String(256), nullable=False),
|
|
50
|
+
sa.Column("manifest_json", postgresql.JSONB, nullable=False),
|
|
51
|
+
sa.Column("error_message", sa.Text, nullable=True),
|
|
52
|
+
sa.Column("url", sa.String(256), nullable=True),
|
|
53
|
+
# Version tracking for redeployments (CI/CD support)
|
|
54
|
+
# - version: increments on each redeploy (1, 2, 3, ...)
|
|
55
|
+
# - is_latest: only one deployment per name should be true
|
|
56
|
+
# - previous_deployment_id: links to the deployment this replaced
|
|
57
|
+
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
|
|
58
|
+
sa.Column("is_latest", sa.Boolean(), nullable=False, server_default="true"),
|
|
59
|
+
sa.Column("previous_deployment_id", sa.String(32), nullable=True),
|
|
60
|
+
# Timestamps
|
|
61
|
+
sa.Column(
|
|
62
|
+
"created_at",
|
|
63
|
+
sa.DateTime(timezone=True),
|
|
64
|
+
nullable=False,
|
|
65
|
+
server_default=sa.func.now(),
|
|
66
|
+
),
|
|
67
|
+
sa.Column(
|
|
68
|
+
"updated_at",
|
|
69
|
+
sa.DateTime(timezone=True),
|
|
70
|
+
nullable=False,
|
|
71
|
+
server_default=sa.func.now(),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
op.create_index("ix_deployments_deployment_id", "deployments", ["deployment_id"])
|
|
75
|
+
op.create_index("ix_deployments_owner_id", "deployments", ["owner_id"])
|
|
76
|
+
op.create_index("ix_deployments_state", "deployments", ["state"])
|
|
77
|
+
op.create_index("ix_deployments_created_at", "deployments", ["created_at"])
|
|
78
|
+
op.create_index("ix_deployments_name", "deployments", ["name"])
|
|
79
|
+
op.create_index("ix_deployments_name_is_latest", "deployments", ["name", "is_latest"])
|
|
80
|
+
|
|
81
|
+
# Partial unique index: only one is_latest=true per (owner_id, name)
|
|
82
|
+
# This ensures at most one active deployment per name per owner
|
|
83
|
+
# Excludes destroyed/failed deployments from the constraint
|
|
84
|
+
# Note: SQLAlchemy with native_enum=False stores enum names (uppercase), not values
|
|
85
|
+
op.execute("""
|
|
86
|
+
CREATE UNIQUE INDEX ix_deployments_unique_active_name
|
|
87
|
+
ON deployments (COALESCE(owner_id, ''), name)
|
|
88
|
+
WHERE is_latest = true AND state NOT IN ('DESTROYED', 'FAILED')
|
|
89
|
+
""")
|
|
90
|
+
|
|
91
|
+
# =========================================================================
|
|
92
|
+
# Provider resources table
|
|
93
|
+
# =========================================================================
|
|
94
|
+
# Maps deployments to provider-specific resources (Fly.io machines, etc.)
|
|
95
|
+
op.create_table(
|
|
96
|
+
"provider_resources",
|
|
97
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
98
|
+
sa.Column(
|
|
99
|
+
"deployment_id",
|
|
100
|
+
postgresql.UUID(as_uuid=True),
|
|
101
|
+
sa.ForeignKey("deployments.id", ondelete="CASCADE"),
|
|
102
|
+
nullable=False,
|
|
103
|
+
unique=True,
|
|
104
|
+
),
|
|
105
|
+
sa.Column("provider", sa.String(32), nullable=False),
|
|
106
|
+
sa.Column("app_name", sa.String(128), nullable=False),
|
|
107
|
+
sa.Column("machine_id", sa.String(128), nullable=False),
|
|
108
|
+
sa.Column("region", sa.String(32), nullable=False),
|
|
109
|
+
sa.Column("image_ref", sa.String(256), nullable=False),
|
|
110
|
+
sa.Column(
|
|
111
|
+
"created_at",
|
|
112
|
+
sa.DateTime(timezone=True),
|
|
113
|
+
nullable=False,
|
|
114
|
+
server_default=sa.func.now(),
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
op.create_index("ix_provider_resources_app_name", "provider_resources", ["app_name"])
|
|
118
|
+
op.create_index("ix_provider_resources_provider", "provider_resources", ["provider"])
|
|
119
|
+
|
|
120
|
+
# =========================================================================
|
|
121
|
+
# Idempotency keys table
|
|
122
|
+
# =========================================================================
|
|
123
|
+
# Maps Idempotency-Key headers to deployments for safe retries
|
|
124
|
+
op.create_table(
|
|
125
|
+
"idempotency_keys",
|
|
126
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
127
|
+
sa.Column("key", sa.String(64), unique=True, nullable=False),
|
|
128
|
+
sa.Column(
|
|
129
|
+
"deployment_id",
|
|
130
|
+
postgresql.UUID(as_uuid=True),
|
|
131
|
+
sa.ForeignKey("deployments.id", ondelete="CASCADE"),
|
|
132
|
+
nullable=False,
|
|
133
|
+
),
|
|
134
|
+
sa.Column(
|
|
135
|
+
"created_at",
|
|
136
|
+
sa.DateTime(timezone=True),
|
|
137
|
+
nullable=False,
|
|
138
|
+
server_default=sa.func.now(),
|
|
139
|
+
),
|
|
140
|
+
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
|
141
|
+
)
|
|
142
|
+
op.create_index("ix_idempotency_keys_key", "idempotency_keys", ["key"])
|
|
143
|
+
op.create_index("ix_idempotency_keys_expires_at", "idempotency_keys", ["expires_at"])
|
|
144
|
+
|
|
145
|
+
# =========================================================================
|
|
146
|
+
# Build logs table
|
|
147
|
+
# =========================================================================
|
|
148
|
+
# Stores build and deploy logs for each deployment
|
|
149
|
+
op.create_table(
|
|
150
|
+
"build_logs",
|
|
151
|
+
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
|
152
|
+
sa.Column(
|
|
153
|
+
"deployment_id",
|
|
154
|
+
postgresql.UUID(as_uuid=True),
|
|
155
|
+
sa.ForeignKey("deployments.id", ondelete="CASCADE"),
|
|
156
|
+
nullable=False,
|
|
157
|
+
),
|
|
158
|
+
sa.Column("log_type", sa.String(32), nullable=False),
|
|
159
|
+
sa.Column("content", sa.Text, nullable=False),
|
|
160
|
+
sa.Column(
|
|
161
|
+
"created_at",
|
|
162
|
+
sa.DateTime(timezone=True),
|
|
163
|
+
nullable=False,
|
|
164
|
+
server_default=sa.func.now(),
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
op.create_index("ix_build_logs_deployment_id", "build_logs", ["deployment_id"])
|
|
168
|
+
op.create_index("ix_build_logs_log_type", "build_logs", ["log_type"])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def downgrade() -> None:
|
|
172
|
+
op.execute("DROP INDEX IF EXISTS ix_deployments_unique_active_name")
|
|
173
|
+
op.drop_table("build_logs")
|
|
174
|
+
op.drop_table("idempotency_keys")
|
|
175
|
+
op.drop_table("provider_resources")
|
|
176
|
+
op.drop_table("deployments")
|