pactown 0.1.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.
- pactown/__init__.py +23 -0
- pactown/cli.py +347 -0
- pactown/config.py +158 -0
- pactown/deploy/__init__.py +17 -0
- pactown/deploy/base.py +263 -0
- pactown/deploy/compose.py +359 -0
- pactown/deploy/docker.py +299 -0
- pactown/deploy/kubernetes.py +449 -0
- pactown/deploy/podman.py +400 -0
- pactown/generator.py +212 -0
- pactown/network.py +245 -0
- pactown/orchestrator.py +455 -0
- pactown/parallel.py +268 -0
- pactown/registry/__init__.py +12 -0
- pactown/registry/client.py +253 -0
- pactown/registry/models.py +150 -0
- pactown/registry/server.py +207 -0
- pactown/resolver.py +160 -0
- pactown/sandbox_manager.py +328 -0
- pactown-0.1.4.dist-info/METADATA +308 -0
- pactown-0.1.4.dist-info/RECORD +24 -0
- pactown-0.1.4.dist-info/WHEEL +4 -0
- pactown-0.1.4.dist-info/entry_points.txt +3 -0
- pactown-0.1.4.dist-info/licenses/LICENSE +201 -0
pactown/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Pactown – Decentralized Service Ecosystem Orchestrator using markpact"""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.4"
|
|
4
|
+
|
|
5
|
+
from .config import EcosystemConfig, ServiceConfig, DependencyConfig
|
|
6
|
+
from .orchestrator import Orchestrator
|
|
7
|
+
from .resolver import DependencyResolver
|
|
8
|
+
from .sandbox_manager import SandboxManager
|
|
9
|
+
from .network import ServiceRegistry, PortAllocator, ServiceEndpoint, find_free_port
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"EcosystemConfig",
|
|
13
|
+
"ServiceConfig",
|
|
14
|
+
"DependencyConfig",
|
|
15
|
+
"Orchestrator",
|
|
16
|
+
"DependencyResolver",
|
|
17
|
+
"SandboxManager",
|
|
18
|
+
"ServiceRegistry",
|
|
19
|
+
"PortAllocator",
|
|
20
|
+
"ServiceEndpoint",
|
|
21
|
+
"find_free_port",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
pactown/cli.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""CLI for pactown ecosystem orchestrator."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .config import EcosystemConfig, load_config
|
|
14
|
+
from .orchestrator import Orchestrator, run_ecosystem
|
|
15
|
+
from .resolver import DependencyResolver
|
|
16
|
+
from .generator import scan_folder, generate_config, print_scan_results
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group()
|
|
23
|
+
@click.version_option(version=__version__, prog_name="pactown")
|
|
24
|
+
def cli():
|
|
25
|
+
"""Pactown – Decentralized Service Ecosystem Orchestrator."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cli.command()
|
|
30
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
31
|
+
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
|
32
|
+
@click.option("--no-health", is_flag=True, help="Don't wait for health checks")
|
|
33
|
+
@click.option("--quiet", "-q", is_flag=True, help="Minimal output")
|
|
34
|
+
@click.option("--sequential", "-s", is_flag=True, help="Disable parallel execution")
|
|
35
|
+
@click.option("--workers", "-w", default=4, type=int, help="Max parallel workers")
|
|
36
|
+
def up(config_path: str, dry_run: bool, no_health: bool, quiet: bool, sequential: bool, workers: int):
|
|
37
|
+
"""Start all services in the ecosystem."""
|
|
38
|
+
try:
|
|
39
|
+
config = load_config(config_path)
|
|
40
|
+
orch = Orchestrator(config, base_path=Path(config_path).parent, verbose=not quiet)
|
|
41
|
+
|
|
42
|
+
if dry_run:
|
|
43
|
+
console.print(f"[bold]Dry run: {config.name}[/bold]\n")
|
|
44
|
+
resolver = DependencyResolver(config)
|
|
45
|
+
order = resolver.get_startup_order()
|
|
46
|
+
|
|
47
|
+
console.print("Would start services in order:")
|
|
48
|
+
for i, name in enumerate(order, 1):
|
|
49
|
+
svc = config.services[name]
|
|
50
|
+
deps = [d.name for d in svc.depends_on]
|
|
51
|
+
deps_str = f" (deps: {', '.join(deps)})" if deps else ""
|
|
52
|
+
console.print(f" {i}. {name}:{svc.port}{deps_str}")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if not orch.validate():
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
orch.start_all(
|
|
59
|
+
wait_for_health=not no_health,
|
|
60
|
+
parallel=not sequential,
|
|
61
|
+
max_workers=workers,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
console.print("\n[dim]Press Ctrl+C to stop all services[/dim]\n")
|
|
65
|
+
try:
|
|
66
|
+
while True:
|
|
67
|
+
import time
|
|
68
|
+
time.sleep(1)
|
|
69
|
+
except KeyboardInterrupt:
|
|
70
|
+
console.print("\n[yellow]Shutting down...[/yellow]")
|
|
71
|
+
orch.stop_all()
|
|
72
|
+
|
|
73
|
+
except Exception as e:
|
|
74
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@cli.command()
|
|
79
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
80
|
+
def down(config_path: str):
|
|
81
|
+
"""Stop all services in the ecosystem."""
|
|
82
|
+
try:
|
|
83
|
+
config = load_config(config_path)
|
|
84
|
+
orch = Orchestrator(config, base_path=Path(config_path).parent)
|
|
85
|
+
orch.stop_all()
|
|
86
|
+
console.print("[green]All services stopped[/green]")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@cli.command()
|
|
93
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
94
|
+
def status(config_path: str):
|
|
95
|
+
"""Show status of all services."""
|
|
96
|
+
try:
|
|
97
|
+
config = load_config(config_path)
|
|
98
|
+
orch = Orchestrator(config, base_path=Path(config_path).parent)
|
|
99
|
+
orch.print_status()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@cli.command()
|
|
106
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
107
|
+
def validate(config_path: str):
|
|
108
|
+
"""Validate ecosystem configuration."""
|
|
109
|
+
try:
|
|
110
|
+
config = load_config(config_path)
|
|
111
|
+
orch = Orchestrator(config, base_path=Path(config_path).parent)
|
|
112
|
+
|
|
113
|
+
if orch.validate():
|
|
114
|
+
sys.exit(0)
|
|
115
|
+
else:
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@cli.command()
|
|
123
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
124
|
+
def graph(config_path: str):
|
|
125
|
+
"""Show dependency graph."""
|
|
126
|
+
try:
|
|
127
|
+
config = load_config(config_path)
|
|
128
|
+
resolver = DependencyResolver(config)
|
|
129
|
+
console.print(Panel(resolver.print_graph(), title="Dependency Graph"))
|
|
130
|
+
except Exception as e:
|
|
131
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@cli.command()
|
|
136
|
+
@click.option("--name", "-n", default="my-ecosystem", help="Ecosystem name")
|
|
137
|
+
@click.option("--output", "-o", default="saas.pactown.yaml", help="Output file")
|
|
138
|
+
def init(name: str, output: str):
|
|
139
|
+
"""Initialize a new pactown ecosystem configuration."""
|
|
140
|
+
config = {
|
|
141
|
+
"name": name,
|
|
142
|
+
"version": "0.1.0",
|
|
143
|
+
"description": f"{name} - A pactown ecosystem",
|
|
144
|
+
"base_port": 8000,
|
|
145
|
+
"sandbox_root": "./.pactown-sandboxes",
|
|
146
|
+
"registry": {
|
|
147
|
+
"url": "http://localhost:8800",
|
|
148
|
+
"namespace": "default",
|
|
149
|
+
},
|
|
150
|
+
"services": {
|
|
151
|
+
"api": {
|
|
152
|
+
"readme": "services/api/README.md",
|
|
153
|
+
"port": 8001,
|
|
154
|
+
"health_check": "/health",
|
|
155
|
+
"env": {},
|
|
156
|
+
"depends_on": [],
|
|
157
|
+
},
|
|
158
|
+
"web": {
|
|
159
|
+
"readme": "services/web/README.md",
|
|
160
|
+
"port": 8002,
|
|
161
|
+
"health_check": "/",
|
|
162
|
+
"depends_on": [
|
|
163
|
+
{"name": "api", "endpoint": "http://localhost:8001"},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
output_path = Path(output)
|
|
170
|
+
with open(output_path, "w") as f:
|
|
171
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
172
|
+
|
|
173
|
+
console.print(f"[green]Created {output_path}[/green]")
|
|
174
|
+
console.print("\nNext steps:")
|
|
175
|
+
console.print(" 1. Create service README.md files")
|
|
176
|
+
console.print(" 2. Run: pactown validate saas.pactown.yaml")
|
|
177
|
+
console.print(" 3. Run: pactown up saas.pactown.yaml")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@cli.command()
|
|
181
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
182
|
+
@click.option("--registry", "-r", default="http://localhost:8800", help="Registry URL")
|
|
183
|
+
def publish(config_path: str, registry: str):
|
|
184
|
+
"""Publish all modules to registry."""
|
|
185
|
+
try:
|
|
186
|
+
from .registry.client import RegistryClient
|
|
187
|
+
|
|
188
|
+
config = load_config(config_path)
|
|
189
|
+
client = RegistryClient(registry)
|
|
190
|
+
|
|
191
|
+
for name, service in config.services.items():
|
|
192
|
+
readme_path = Path(config_path).parent / service.readme
|
|
193
|
+
if readme_path.exists():
|
|
194
|
+
result = client.publish(
|
|
195
|
+
name=name,
|
|
196
|
+
version=config.version,
|
|
197
|
+
readme_path=readme_path,
|
|
198
|
+
namespace=config.registry.namespace,
|
|
199
|
+
)
|
|
200
|
+
if result.get("success"):
|
|
201
|
+
console.print(f"[green]✓ Published {name}@{config.version}[/green]")
|
|
202
|
+
else:
|
|
203
|
+
console.print(f"[red]✗ Failed to publish {name}: {result.get('error')}[/red]")
|
|
204
|
+
except Exception as e:
|
|
205
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@cli.command()
|
|
210
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
211
|
+
@click.option("--registry", "-r", default="http://localhost:8800", help="Registry URL")
|
|
212
|
+
def pull(config_path: str, registry: str):
|
|
213
|
+
"""Pull dependencies from registry."""
|
|
214
|
+
try:
|
|
215
|
+
from .registry.client import RegistryClient
|
|
216
|
+
|
|
217
|
+
config = load_config(config_path)
|
|
218
|
+
client = RegistryClient(registry)
|
|
219
|
+
|
|
220
|
+
for name, service in config.services.items():
|
|
221
|
+
for dep in service.depends_on:
|
|
222
|
+
if dep.name not in config.services:
|
|
223
|
+
result = client.pull(dep.name, dep.version)
|
|
224
|
+
if result:
|
|
225
|
+
console.print(f"[green]✓ Pulled {dep.name}@{dep.version}[/green]")
|
|
226
|
+
else:
|
|
227
|
+
console.print(f"[yellow]⚠ {dep.name} not found in registry[/yellow]")
|
|
228
|
+
except Exception as e:
|
|
229
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@cli.command()
|
|
234
|
+
@click.argument("folder", type=click.Path(exists=True))
|
|
235
|
+
def scan(folder: str):
|
|
236
|
+
"""Scan a folder and show detected services."""
|
|
237
|
+
print_scan_results(Path(folder))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@cli.command()
|
|
241
|
+
@click.argument("folder", type=click.Path(exists=True))
|
|
242
|
+
@click.option("--name", "-n", help="Ecosystem name (default: folder name)")
|
|
243
|
+
@click.option("--output", "-o", default="saas.pactown.yaml", help="Output file")
|
|
244
|
+
@click.option("--base-port", "-p", default=8000, type=int, help="Starting port")
|
|
245
|
+
def generate(folder: str, name: Optional[str], output: str, base_port: int):
|
|
246
|
+
"""Generate pactown config from a folder of README.md files.
|
|
247
|
+
|
|
248
|
+
Example:
|
|
249
|
+
pactown generate ./examples -o my-ecosystem.pactown.yaml
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
folder_path = Path(folder)
|
|
253
|
+
output_path = Path(output)
|
|
254
|
+
|
|
255
|
+
console.print(f"[bold]Scanning {folder_path}...[/bold]\n")
|
|
256
|
+
print_scan_results(folder_path)
|
|
257
|
+
|
|
258
|
+
console.print()
|
|
259
|
+
config = generate_config(
|
|
260
|
+
folder=folder_path,
|
|
261
|
+
name=name,
|
|
262
|
+
base_port=base_port,
|
|
263
|
+
output=output_path,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
console.print(f"\n[green]✓ Generated {output_path}[/green]")
|
|
267
|
+
console.print(f" Services: {len(config['services'])}")
|
|
268
|
+
console.print(f"\nNext steps:")
|
|
269
|
+
console.print(f" pactown validate {output}")
|
|
270
|
+
console.print(f" pactown up {output}")
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@cli.command()
|
|
278
|
+
@click.argument("config_path", type=click.Path(exists=True))
|
|
279
|
+
@click.option("--output", "-o", default=".", help="Output directory")
|
|
280
|
+
@click.option("--production", "-p", is_flag=True, help="Generate production config")
|
|
281
|
+
@click.option("--kubernetes", "-k", is_flag=True, help="Generate Kubernetes manifests")
|
|
282
|
+
def deploy(config_path: str, output: str, production: bool, kubernetes: bool):
|
|
283
|
+
"""Generate deployment files (Docker Compose, Kubernetes)."""
|
|
284
|
+
try:
|
|
285
|
+
from .deploy.compose import generate_compose_from_config
|
|
286
|
+
from .deploy.kubernetes import KubernetesBackend
|
|
287
|
+
from .deploy.base import DeploymentConfig
|
|
288
|
+
|
|
289
|
+
config_path = Path(config_path)
|
|
290
|
+
output_dir = Path(output)
|
|
291
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
|
|
293
|
+
if kubernetes:
|
|
294
|
+
# Generate Kubernetes manifests
|
|
295
|
+
from .config import load_config
|
|
296
|
+
ecosystem = load_config(config_path)
|
|
297
|
+
deploy_config = DeploymentConfig.for_production() if production else DeploymentConfig.for_development()
|
|
298
|
+
k8s = KubernetesBackend(deploy_config)
|
|
299
|
+
|
|
300
|
+
k8s_dir = output_dir / "kubernetes"
|
|
301
|
+
k8s_dir.mkdir(exist_ok=True)
|
|
302
|
+
|
|
303
|
+
for name, service in ecosystem.services.items():
|
|
304
|
+
image = f"{deploy_config.image_prefix}/{name}:latest"
|
|
305
|
+
manifests = k8s.generate_manifests(
|
|
306
|
+
service_name=name,
|
|
307
|
+
image_name=image,
|
|
308
|
+
port=service.port or 8000,
|
|
309
|
+
env=service.env,
|
|
310
|
+
health_check=service.health_check,
|
|
311
|
+
)
|
|
312
|
+
k8s.save_manifests(name, manifests, k8s_dir)
|
|
313
|
+
console.print(f" [green]✓[/green] {k8s_dir}/{name}.yaml")
|
|
314
|
+
|
|
315
|
+
console.print(f"\n[green]Generated Kubernetes manifests in {k8s_dir}[/green]")
|
|
316
|
+
console.print(f"\nDeploy with:")
|
|
317
|
+
console.print(f" kubectl apply -f {k8s_dir}/")
|
|
318
|
+
else:
|
|
319
|
+
# Generate Docker Compose
|
|
320
|
+
generate_compose_from_config(
|
|
321
|
+
config_path=config_path,
|
|
322
|
+
output_dir=output_dir,
|
|
323
|
+
production=production,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
console.print(f"\n[green]Generated Docker Compose files in {output_dir}[/green]")
|
|
327
|
+
console.print(f"\nRun with:")
|
|
328
|
+
if production:
|
|
329
|
+
console.print(f" docker compose -f docker-compose.yaml -f docker-compose.prod.yaml up -d")
|
|
330
|
+
else:
|
|
331
|
+
console.print(f" docker compose up -d")
|
|
332
|
+
console.print(f" # or: podman-compose up -d")
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
336
|
+
import traceback
|
|
337
|
+
traceback.print_exc()
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def main(argv=None):
|
|
342
|
+
"""Main entry point."""
|
|
343
|
+
cli(argv)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
main()
|
pactown/config.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Configuration models for pactown ecosystems."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Any
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DependencyConfig:
|
|
11
|
+
"""Configuration for a service dependency."""
|
|
12
|
+
name: str
|
|
13
|
+
version: str = "*"
|
|
14
|
+
registry: str = "local"
|
|
15
|
+
endpoint: Optional[str] = None
|
|
16
|
+
env_var: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_dict(cls, data: dict | str) -> "DependencyConfig":
|
|
20
|
+
if isinstance(data, str):
|
|
21
|
+
parts = data.split("@")
|
|
22
|
+
name = parts[0]
|
|
23
|
+
version = parts[1] if len(parts) > 1 else "*"
|
|
24
|
+
return cls(name=name, version=version)
|
|
25
|
+
return cls(**data)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ServiceConfig:
|
|
30
|
+
"""Configuration for a single service in the ecosystem."""
|
|
31
|
+
name: str
|
|
32
|
+
readme: str
|
|
33
|
+
port: Optional[int] = None
|
|
34
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
35
|
+
depends_on: list[DependencyConfig] = field(default_factory=list)
|
|
36
|
+
health_check: Optional[str] = None
|
|
37
|
+
replicas: int = 1
|
|
38
|
+
sandbox_path: Optional[str] = None
|
|
39
|
+
auto_restart: bool = True
|
|
40
|
+
timeout: int = 60
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(cls, name: str, data: dict) -> "ServiceConfig":
|
|
44
|
+
deps = []
|
|
45
|
+
for dep in data.get("depends_on", []):
|
|
46
|
+
deps.append(DependencyConfig.from_dict(dep))
|
|
47
|
+
|
|
48
|
+
return cls(
|
|
49
|
+
name=name,
|
|
50
|
+
readme=data.get("readme", f"{name}/README.md"),
|
|
51
|
+
port=data.get("port"),
|
|
52
|
+
env=data.get("env", {}),
|
|
53
|
+
depends_on=deps,
|
|
54
|
+
health_check=data.get("health_check"),
|
|
55
|
+
replicas=data.get("replicas", 1),
|
|
56
|
+
sandbox_path=data.get("sandbox_path"),
|
|
57
|
+
auto_restart=data.get("auto_restart", True),
|
|
58
|
+
timeout=data.get("timeout", 60),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class RegistryConfig:
|
|
64
|
+
"""Configuration for artifact registry."""
|
|
65
|
+
url: str = "http://localhost:8800"
|
|
66
|
+
auth_token: Optional[str] = None
|
|
67
|
+
namespace: str = "default"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class EcosystemConfig:
|
|
72
|
+
"""Configuration for a complete pactown ecosystem."""
|
|
73
|
+
name: str
|
|
74
|
+
version: str = "0.1.0"
|
|
75
|
+
description: str = ""
|
|
76
|
+
services: dict[str, ServiceConfig] = field(default_factory=dict)
|
|
77
|
+
registry: RegistryConfig = field(default_factory=RegistryConfig)
|
|
78
|
+
base_port: int = 8000
|
|
79
|
+
sandbox_root: str = "./.pactown-sandboxes"
|
|
80
|
+
network: str = "pactown-net"
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_yaml(cls, path: Path) -> "EcosystemConfig":
|
|
84
|
+
"""Load ecosystem configuration from YAML file."""
|
|
85
|
+
with open(path) as f:
|
|
86
|
+
data = yaml.safe_load(f)
|
|
87
|
+
return cls.from_dict(data, base_path=path.parent)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_dict(cls, data: dict, base_path: Optional[Path] = None) -> "EcosystemConfig":
|
|
91
|
+
"""Create configuration from dictionary."""
|
|
92
|
+
services = {}
|
|
93
|
+
base_port = data.get("base_port", 8000)
|
|
94
|
+
|
|
95
|
+
for i, (name, svc_data) in enumerate(data.get("services", {}).items()):
|
|
96
|
+
if svc_data.get("port") is None:
|
|
97
|
+
svc_data["port"] = base_port + i
|
|
98
|
+
services[name] = ServiceConfig.from_dict(name, svc_data)
|
|
99
|
+
|
|
100
|
+
registry_data = data.get("registry", {})
|
|
101
|
+
registry = RegistryConfig(
|
|
102
|
+
url=registry_data.get("url", "http://localhost:8800"),
|
|
103
|
+
auth_token=registry_data.get("auth_token"),
|
|
104
|
+
namespace=registry_data.get("namespace", "default"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return cls(
|
|
108
|
+
name=data.get("name", "unnamed-ecosystem"),
|
|
109
|
+
version=data.get("version", "0.1.0"),
|
|
110
|
+
description=data.get("description", ""),
|
|
111
|
+
services=services,
|
|
112
|
+
registry=registry,
|
|
113
|
+
base_port=base_port,
|
|
114
|
+
sandbox_root=data.get("sandbox_root", "./.pactown-sandboxes"),
|
|
115
|
+
network=data.get("network", "pactown-net"),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def to_dict(self) -> dict:
|
|
119
|
+
"""Convert configuration to dictionary."""
|
|
120
|
+
return {
|
|
121
|
+
"name": self.name,
|
|
122
|
+
"version": self.version,
|
|
123
|
+
"description": self.description,
|
|
124
|
+
"base_port": self.base_port,
|
|
125
|
+
"sandbox_root": self.sandbox_root,
|
|
126
|
+
"network": self.network,
|
|
127
|
+
"registry": {
|
|
128
|
+
"url": self.registry.url,
|
|
129
|
+
"namespace": self.registry.namespace,
|
|
130
|
+
},
|
|
131
|
+
"services": {
|
|
132
|
+
name: {
|
|
133
|
+
"readme": svc.readme,
|
|
134
|
+
"port": svc.port,
|
|
135
|
+
"env": svc.env,
|
|
136
|
+
"depends_on": [
|
|
137
|
+
{"name": d.name, "version": d.version, "endpoint": d.endpoint}
|
|
138
|
+
for d in svc.depends_on
|
|
139
|
+
],
|
|
140
|
+
"health_check": svc.health_check,
|
|
141
|
+
"replicas": svc.replicas,
|
|
142
|
+
}
|
|
143
|
+
for name, svc in self.services.items()
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def to_yaml(self, path: Path) -> None:
|
|
148
|
+
"""Save configuration to YAML file."""
|
|
149
|
+
with open(path, "w") as f:
|
|
150
|
+
yaml.dump(self.to_dict(), f, default_flow_style=False, sort_keys=False)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def load_config(path: str | Path) -> EcosystemConfig:
|
|
154
|
+
"""Load ecosystem configuration from file."""
|
|
155
|
+
path = Path(path)
|
|
156
|
+
if not path.exists():
|
|
157
|
+
raise FileNotFoundError(f"Config file not found: {path}")
|
|
158
|
+
return EcosystemConfig.from_yaml(path)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Deployment backends for pactown - Docker, Podman, Kubernetes, etc."""
|
|
2
|
+
|
|
3
|
+
from .base import DeploymentBackend, DeploymentConfig, DeploymentResult
|
|
4
|
+
from .docker import DockerBackend
|
|
5
|
+
from .podman import PodmanBackend
|
|
6
|
+
from .kubernetes import KubernetesBackend
|
|
7
|
+
from .compose import ComposeGenerator
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DeploymentBackend",
|
|
11
|
+
"DeploymentConfig",
|
|
12
|
+
"DeploymentResult",
|
|
13
|
+
"DockerBackend",
|
|
14
|
+
"PodmanBackend",
|
|
15
|
+
"KubernetesBackend",
|
|
16
|
+
"ComposeGenerator",
|
|
17
|
+
]
|