ots-containers 0.3.0__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.
- ots_containers/__init__.py +3 -0
- ots_containers/assets.py +32 -0
- ots_containers/cli.py +86 -0
- ots_containers/commands/README.md +124 -0
- ots_containers/commands/__init__.py +9 -0
- ots_containers/commands/assets.py +29 -0
- ots_containers/commands/cloudinit/__init__.py +6 -0
- ots_containers/commands/cloudinit/app.py +158 -0
- ots_containers/commands/cloudinit/templates.py +127 -0
- ots_containers/commands/common.py +71 -0
- ots_containers/commands/env/__init__.py +6 -0
- ots_containers/commands/env/app.py +348 -0
- ots_containers/commands/image/__init__.py +6 -0
- ots_containers/commands/image/app.py +1137 -0
- ots_containers/commands/init.py +253 -0
- ots_containers/commands/instance/__init__.py +57 -0
- ots_containers/commands/instance/_helpers.py +161 -0
- ots_containers/commands/instance/annotations.py +111 -0
- ots_containers/commands/instance/app.py +935 -0
- ots_containers/commands/proxy/__init__.py +11 -0
- ots_containers/commands/proxy/_helpers.py +95 -0
- ots_containers/commands/proxy/app.py +160 -0
- ots_containers/commands/service/__init__.py +6 -0
- ots_containers/commands/service/_helpers.py +294 -0
- ots_containers/commands/service/app.py +498 -0
- ots_containers/commands/service/packages.py +217 -0
- ots_containers/config.py +158 -0
- ots_containers/db.py +582 -0
- ots_containers/environment_file.py +491 -0
- ots_containers/podman.py +60 -0
- ots_containers/quadlet.py +341 -0
- ots_containers/systemd.py +217 -0
- ots_containers-0.3.0.dist-info/METADATA +284 -0
- ots_containers-0.3.0.dist-info/RECORD +37 -0
- ots_containers-0.3.0.dist-info/WHEEL +4 -0
- ots_containers-0.3.0.dist-info/entry_points.txt +2 -0
- ots_containers-0.3.0.dist-info/licenses/LICENSE +21 -0
ots_containers/assets.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# src/ots_containers/assets.py
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .config import Config
|
|
7
|
+
from .podman import podman
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def update(cfg: Config, create_volume: bool = True) -> None:
|
|
11
|
+
if create_volume:
|
|
12
|
+
podman.volume.create("static_assets", check=False)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
result = podman.volume.mount("static_assets", capture_output=True, text=True, check=True)
|
|
16
|
+
except subprocess.CalledProcessError as e:
|
|
17
|
+
stderr = e.stderr.strip() if e.stderr else "unknown error"
|
|
18
|
+
raise SystemExit(f"Failed to mount volume 'static_assets': {stderr}")
|
|
19
|
+
assets_dir = Path(result.stdout.strip())
|
|
20
|
+
|
|
21
|
+
result = podman.create(cfg.resolved_image_with_tag, capture_output=True, text=True, check=True)
|
|
22
|
+
container_id = result.stdout.strip()
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
podman.cp(f"{container_id}:/app/public/.", str(assets_dir), check=True)
|
|
26
|
+
manifest = assets_dir / "web/dist/.vite/manifest.json"
|
|
27
|
+
if manifest.exists():
|
|
28
|
+
print(f"Manifest found: {manifest}")
|
|
29
|
+
else:
|
|
30
|
+
print(f"Warning: manifest not found at {manifest}")
|
|
31
|
+
finally:
|
|
32
|
+
podman.rm(container_id, check=True)
|
ots_containers/cli.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# src/ots_containers/cli.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Manage OTS Podman containers via Quadlets.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
|
|
8
|
+
ots-containers init
|
|
9
|
+
ots-containers image pull --tag v0.23.0 --current
|
|
10
|
+
ots-containers instance deploy 7043
|
|
11
|
+
ots-containers instance redeploy 7044
|
|
12
|
+
ots-containers image rollback
|
|
13
|
+
ots-containers instance redeploy
|
|
14
|
+
ots-containers assets sync
|
|
15
|
+
|
|
16
|
+
# Or run directly without installing:
|
|
17
|
+
$ cd src/
|
|
18
|
+
$ pip install -e .
|
|
19
|
+
$ python -m ots_containers.cli instance deploy 7043
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import cyclopts
|
|
23
|
+
|
|
24
|
+
from . import __version__
|
|
25
|
+
from .commands import assets as assets_cmd
|
|
26
|
+
from .commands import cloudinit, env, image, init, instance, proxy, service
|
|
27
|
+
from .podman import podman
|
|
28
|
+
|
|
29
|
+
app = cyclopts.App(
|
|
30
|
+
name="ots-containers",
|
|
31
|
+
help="Service orchestration for OTS: Podman Quadlets and systemd services",
|
|
32
|
+
version=__version__,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Register topic sub-apps
|
|
36
|
+
app.command(init.app)
|
|
37
|
+
app.command(instance.app)
|
|
38
|
+
app.command(image.app)
|
|
39
|
+
app.command(assets_cmd.app)
|
|
40
|
+
app.command(proxy.app)
|
|
41
|
+
app.command(service.app)
|
|
42
|
+
app.command(cloudinit.app)
|
|
43
|
+
app.command(env.app)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.default
|
|
47
|
+
def _default():
|
|
48
|
+
"""Show help when no command is specified."""
|
|
49
|
+
app.help_print([])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Root-level command for quick access
|
|
53
|
+
@app.command
|
|
54
|
+
def ps():
|
|
55
|
+
"""Show running OTS containers (podman view)."""
|
|
56
|
+
podman.ps(
|
|
57
|
+
filter="name=onetime",
|
|
58
|
+
format="table {{.ID}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.command
|
|
63
|
+
def version():
|
|
64
|
+
"""Show version and build info."""
|
|
65
|
+
import subprocess
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
|
|
68
|
+
print(f"ots-containers {__version__}")
|
|
69
|
+
|
|
70
|
+
# Try to get git info if available
|
|
71
|
+
try:
|
|
72
|
+
pkg_dir = Path(__file__).parent
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
["git", "-C", str(pkg_dir), "rev-parse", "--short", "HEAD"],
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
)
|
|
78
|
+
if result.returncode == 0:
|
|
79
|
+
commit = result.stdout.strip()
|
|
80
|
+
print(f"git commit: {commit}")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
app()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# CLI Style Guide
|
|
2
|
+
|
|
3
|
+
## Command Structure
|
|
4
|
+
|
|
5
|
+
We follow a Heroku-style `topic:command` pattern:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
ots-containers <topic> <command> [identifiers] [flags]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Instance Types
|
|
12
|
+
|
|
13
|
+
Three container types, each with explicit naming:
|
|
14
|
+
|
|
15
|
+
| Type | Systemd Unit | Identifier | Use |
|
|
16
|
+
|------|--------------|------------|-----|
|
|
17
|
+
| `web` | `onetime-web@{port}` | Port number | HTTP servers |
|
|
18
|
+
| `worker` | `onetime-worker@{id}` | Name or number | Background jobs |
|
|
19
|
+
| `scheduler` | `onetime-scheduler@{id}` | Name or number | Scheduled tasks |
|
|
20
|
+
|
|
21
|
+
## Command Syntax
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Positional identifiers with type flag
|
|
25
|
+
ots instances restart --web 7043 7044
|
|
26
|
+
ots instances restart --worker billing emails
|
|
27
|
+
ots instances restart --scheduler main
|
|
28
|
+
|
|
29
|
+
# Auto-discover all types when no args
|
|
30
|
+
ots instances stop # stops ALL running instances
|
|
31
|
+
ots instances status # shows ALL configured instances
|
|
32
|
+
|
|
33
|
+
# Type-specific discovery
|
|
34
|
+
ots instances status --web # only web instances
|
|
35
|
+
ots instances logs --scheduler -f # only scheduler logs
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Topics
|
|
39
|
+
|
|
40
|
+
Each topic is a separate module with its own `cyclopts.App`:
|
|
41
|
+
|
|
42
|
+
| Topic | Purpose |
|
|
43
|
+
|-------|---------|
|
|
44
|
+
| `instance` | Container lifecycle and runtime control |
|
|
45
|
+
| `service` | Native systemd services (Valkey, Redis) |
|
|
46
|
+
| `image` | Container image management |
|
|
47
|
+
| `assets` | Static asset management |
|
|
48
|
+
| `cloudinit` | Cloud-init configuration generation |
|
|
49
|
+
| `env` | Environment file management |
|
|
50
|
+
|
|
51
|
+
To add a new topic, create a module and register it in `cli.py`.
|
|
52
|
+
|
|
53
|
+
## Two-Level Abstraction
|
|
54
|
+
|
|
55
|
+
Commands are categorized by their impact:
|
|
56
|
+
|
|
57
|
+
### High-level (affects config + state)
|
|
58
|
+
Commands that modify quadlet templates, database records, or both:
|
|
59
|
+
- `deploy`, `redeploy`, `undeploy`
|
|
60
|
+
|
|
61
|
+
These commands should document their config impact in the docstring.
|
|
62
|
+
|
|
63
|
+
### Low-level (runtime control only)
|
|
64
|
+
Commands that only interact with systemd, no config changes:
|
|
65
|
+
- `start`, `stop`, `restart`, `status`, `logs`, `enable`, `disable`, `exec`
|
|
66
|
+
|
|
67
|
+
These commands should explicitly state they do NOT refresh config.
|
|
68
|
+
|
|
69
|
+
## Naming Conventions
|
|
70
|
+
|
|
71
|
+
| Pattern | Example | Use for |
|
|
72
|
+
|---------|---------|---------|
|
|
73
|
+
| Verb | `deploy`, `sync` | Actions |
|
|
74
|
+
| `--flag` | `--force`, `--yes` | Boolean options |
|
|
75
|
+
| `--option VALUE` | `--delay 5`, `--lines 50` | Value options |
|
|
76
|
+
| `--type` shortcuts | `--web`, `--worker`, `--scheduler` | Instance type selection |
|
|
77
|
+
|
|
78
|
+
## Default Commands
|
|
79
|
+
|
|
80
|
+
Use `@app.default` for the "list" operation when invoking a topic without a subcommand:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
@app.default
|
|
84
|
+
def list_instances(
|
|
85
|
+
identifiers: Identifiers = (),
|
|
86
|
+
web: WebFlag = False,
|
|
87
|
+
worker: WorkerFlag = False,
|
|
88
|
+
scheduler: SchedulerFlag = False,
|
|
89
|
+
):
|
|
90
|
+
"""List instances with status and deployment info."""
|
|
91
|
+
...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This follows Heroku's pattern where `heroku apps` lists apps.
|
|
95
|
+
|
|
96
|
+
## Help Text
|
|
97
|
+
|
|
98
|
+
First line: Brief imperative description.
|
|
99
|
+
Blank line, then: Config impact and usage notes.
|
|
100
|
+
Include Examples section with common use cases:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
@app.command
|
|
104
|
+
def redeploy(...):
|
|
105
|
+
"""Regenerate quadlet and restart containers.
|
|
106
|
+
|
|
107
|
+
Rewrites quadlet config, restarts service. Records to timeline for audit.
|
|
108
|
+
Use --force to fully teardown and recreate.
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
ots instances redeploy # Redeploy all running
|
|
112
|
+
ots instances redeploy --web # Redeploy web instances
|
|
113
|
+
ots instances redeploy --web 7043 7044 # Redeploy specific web
|
|
114
|
+
ots instances redeploy --scheduler main # Redeploy specific scheduler
|
|
115
|
+
"""
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Adding Commands
|
|
119
|
+
|
|
120
|
+
1. Add to existing topic module, or create new topic
|
|
121
|
+
2. Use shared helpers from `_helpers.py` (`resolve_identifiers`, `for_each_instance`)
|
|
122
|
+
3. Use type annotations from `annotations.py` (`Identifiers`, `WebFlag`, etc.)
|
|
123
|
+
4. Document config impact and include Examples in docstring
|
|
124
|
+
5. Register new topics in `cli.py` via `app.command(topic.app)`
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# src/ots_containers/commands/__init__.py
|
|
2
|
+
"""Command topic modules for ots-containers CLI."""
|
|
3
|
+
|
|
4
|
+
from . import assets as assets
|
|
5
|
+
from . import image as image
|
|
6
|
+
from . import init as init
|
|
7
|
+
from . import instance as instance
|
|
8
|
+
from . import proxy as proxy
|
|
9
|
+
from . import service as service
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# src/ots_containers/commands/assets.py
|
|
2
|
+
"""Asset management commands for OTS containers."""
|
|
3
|
+
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import cyclopts
|
|
7
|
+
|
|
8
|
+
from .. import assets as assets_module
|
|
9
|
+
from ..config import Config
|
|
10
|
+
|
|
11
|
+
app = cyclopts.App(name="assets", help="Extract web assets from container image to volume")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command
|
|
15
|
+
def sync(
|
|
16
|
+
create_volume: Annotated[
|
|
17
|
+
bool,
|
|
18
|
+
cyclopts.Parameter(help="Create volume if it doesn't exist (use on first deploy)"),
|
|
19
|
+
] = False,
|
|
20
|
+
):
|
|
21
|
+
"""Copy /app/public from container image to static_assets podman volume.
|
|
22
|
+
|
|
23
|
+
Extracts web assets (JS, CSS, images) from the OTS container image
|
|
24
|
+
to a shared volume that Caddy serves directly. Use --create-volume
|
|
25
|
+
on initial setup.
|
|
26
|
+
"""
|
|
27
|
+
cfg = Config()
|
|
28
|
+
cfg.validate()
|
|
29
|
+
assets_module.update(cfg, create_volume=create_volume)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# src/ots_containers/commands/cloudinit/app.py
|
|
2
|
+
"""Cloud-init configuration generation commands.
|
|
3
|
+
|
|
4
|
+
Generates cloud-init YAML with Debian 13 (Trixie) DEB822-style apt sources.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
import cyclopts
|
|
12
|
+
|
|
13
|
+
from .templates import generate_cloudinit_config
|
|
14
|
+
|
|
15
|
+
app = cyclopts.App(
|
|
16
|
+
name="cloudinit",
|
|
17
|
+
help="Generate cloud-init configurations for OTS infrastructure",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.default
|
|
22
|
+
def _default():
|
|
23
|
+
"""Show cloud-init commands help."""
|
|
24
|
+
print("Cloud-init configuration generation for OTS infrastructure")
|
|
25
|
+
print()
|
|
26
|
+
print("Supports Debian 13 (Trixie) DEB822-style apt sources")
|
|
27
|
+
print()
|
|
28
|
+
print("Use 'ots-containers cloudinit --help' for available commands")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command
|
|
32
|
+
def generate(
|
|
33
|
+
output: Annotated[
|
|
34
|
+
str | None,
|
|
35
|
+
cyclopts.Parameter(help="Output file path (default: stdout)"),
|
|
36
|
+
] = None,
|
|
37
|
+
*,
|
|
38
|
+
include_postgresql: Annotated[
|
|
39
|
+
bool, cyclopts.Parameter(help="Include PostgreSQL apt repository")
|
|
40
|
+
] = False,
|
|
41
|
+
include_valkey: Annotated[
|
|
42
|
+
bool, cyclopts.Parameter(help="Include Valkey apt repository")
|
|
43
|
+
] = False,
|
|
44
|
+
postgresql_key: Annotated[
|
|
45
|
+
str | None,
|
|
46
|
+
cyclopts.Parameter(help="Path to PostgreSQL GPG key file"),
|
|
47
|
+
] = None,
|
|
48
|
+
valkey_key: Annotated[
|
|
49
|
+
str | None,
|
|
50
|
+
cyclopts.Parameter(help="Path to Valkey GPG key file"),
|
|
51
|
+
] = None,
|
|
52
|
+
):
|
|
53
|
+
"""Generate cloud-init configuration with Debian 13 apt sources.
|
|
54
|
+
|
|
55
|
+
Generates a cloud-init YAML file with:
|
|
56
|
+
- Debian 13 (Trixie) main repositories in DEB822 format
|
|
57
|
+
- Optional PostgreSQL official repository
|
|
58
|
+
- Optional Valkey repository
|
|
59
|
+
- Package update/upgrade configuration
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
ots-containers cloudinit generate > user-data.yaml
|
|
63
|
+
ots-containers cloudinit generate --output /tmp/cloud-init.yaml
|
|
64
|
+
ots-containers cloudinit generate --include-postgresql --postgresql-key /path/to/pgp.asc
|
|
65
|
+
"""
|
|
66
|
+
# Read GPG keys if provided
|
|
67
|
+
postgresql_gpg = None
|
|
68
|
+
valkey_gpg = None
|
|
69
|
+
|
|
70
|
+
if include_postgresql and postgresql_key:
|
|
71
|
+
postgresql_gpg = Path(postgresql_key).read_text()
|
|
72
|
+
elif include_postgresql:
|
|
73
|
+
print(
|
|
74
|
+
"Warning: --include-postgresql specified but no --postgresql-key provided",
|
|
75
|
+
file=sys.stderr,
|
|
76
|
+
)
|
|
77
|
+
print("PostgreSQL repository will use inline key placeholder", file=sys.stderr)
|
|
78
|
+
|
|
79
|
+
if include_valkey and valkey_key:
|
|
80
|
+
valkey_gpg = Path(valkey_key).read_text()
|
|
81
|
+
elif include_valkey:
|
|
82
|
+
print(
|
|
83
|
+
"Warning: --include-valkey specified but no --valkey-key provided",
|
|
84
|
+
file=sys.stderr,
|
|
85
|
+
)
|
|
86
|
+
print("Valkey repository will use inline key placeholder", file=sys.stderr)
|
|
87
|
+
|
|
88
|
+
# Generate configuration
|
|
89
|
+
config = generate_cloudinit_config(
|
|
90
|
+
include_postgresql=include_postgresql,
|
|
91
|
+
include_valkey=include_valkey,
|
|
92
|
+
postgresql_gpg_key=postgresql_gpg,
|
|
93
|
+
valkey_gpg_key=valkey_gpg,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Output
|
|
97
|
+
if output:
|
|
98
|
+
Path(output).write_text(config)
|
|
99
|
+
print(f"[created] {output}")
|
|
100
|
+
else:
|
|
101
|
+
print(config)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@app.command
|
|
105
|
+
def validate(
|
|
106
|
+
file_path: Annotated[
|
|
107
|
+
str,
|
|
108
|
+
cyclopts.Parameter(help="Path to cloud-init YAML file"),
|
|
109
|
+
],
|
|
110
|
+
):
|
|
111
|
+
"""Validate a cloud-init configuration file.
|
|
112
|
+
|
|
113
|
+
Checks for:
|
|
114
|
+
- Valid YAML syntax
|
|
115
|
+
- Required cloud-init sections
|
|
116
|
+
- DEB822 apt sources format
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
ots-containers cloudinit validate user-data.yaml
|
|
120
|
+
"""
|
|
121
|
+
import yaml
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
config_path = Path(file_path)
|
|
125
|
+
if not config_path.exists():
|
|
126
|
+
print(f"[error] File not found: {file_path}", file=sys.stderr)
|
|
127
|
+
sys.exit(1)
|
|
128
|
+
|
|
129
|
+
content = config_path.read_text()
|
|
130
|
+
data = yaml.safe_load(content)
|
|
131
|
+
|
|
132
|
+
# Basic validation
|
|
133
|
+
errors = []
|
|
134
|
+
|
|
135
|
+
if not isinstance(data, dict):
|
|
136
|
+
errors.append("Root element must be a dictionary")
|
|
137
|
+
|
|
138
|
+
if "apt" in data and "sources_list" in data["apt"]:
|
|
139
|
+
sources_list = data["apt"]["sources_list"]
|
|
140
|
+
# Check for DEB822 format indicators
|
|
141
|
+
if "Types:" not in sources_list and "URIs:" not in sources_list:
|
|
142
|
+
errors.append("apt.sources_list doesn't appear to use DEB822 format")
|
|
143
|
+
|
|
144
|
+
if errors:
|
|
145
|
+
print(f"[error] Validation failed for {file_path}:", file=sys.stderr)
|
|
146
|
+
for error in errors:
|
|
147
|
+
print(f" - {error}", file=sys.stderr)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
else:
|
|
150
|
+
print(f"[ok] {file_path} is valid")
|
|
151
|
+
|
|
152
|
+
except yaml.YAMLError as e:
|
|
153
|
+
print(f"[error] Invalid YAML in {file_path}:", file=sys.stderr)
|
|
154
|
+
print(f" {e}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"[error] Validation failed: {e}", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# src/ots_containers/commands/cloudinit/templates.py
|
|
2
|
+
"""Cloud-init configuration templates with Debian 13 DEB822 apt sources."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_debian13_sources_list() -> str:
|
|
6
|
+
"""Get just the Debian 13 DEB822 sources.list content.
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
DEB822-formatted sources.list content
|
|
10
|
+
"""
|
|
11
|
+
return """Types: deb
|
|
12
|
+
URIs: http://deb.debian.org/debian
|
|
13
|
+
Suites: trixie trixie-updates
|
|
14
|
+
Components: main contrib non-free non-free-firmware
|
|
15
|
+
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
16
|
+
|
|
17
|
+
Types: deb
|
|
18
|
+
URIs: http://deb.debian.org/debian
|
|
19
|
+
Suites: trixie-backports
|
|
20
|
+
Components: main contrib non-free non-free-firmware
|
|
21
|
+
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
22
|
+
|
|
23
|
+
Types: deb
|
|
24
|
+
URIs: http://security.debian.org/debian-security
|
|
25
|
+
Suites: trixie-security
|
|
26
|
+
Components: main contrib non-free non-free-firmware
|
|
27
|
+
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def generate_cloudinit_config(
|
|
32
|
+
*,
|
|
33
|
+
include_postgresql: bool = False,
|
|
34
|
+
include_valkey: bool = False,
|
|
35
|
+
postgresql_gpg_key: str | None = None,
|
|
36
|
+
valkey_gpg_key: str | None = None,
|
|
37
|
+
) -> str:
|
|
38
|
+
"""Generate cloud-init YAML with Debian 13 DEB822-style apt sources.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
include_postgresql: Include PostgreSQL official repository
|
|
42
|
+
include_valkey: Include Valkey repository
|
|
43
|
+
postgresql_gpg_key: PostgreSQL GPG public key content
|
|
44
|
+
valkey_gpg_key: Valkey GPG public key content
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Complete cloud-init YAML configuration as string
|
|
48
|
+
"""
|
|
49
|
+
# Base configuration with Debian 13 main repositories
|
|
50
|
+
# Get the sources list and indent it for YAML
|
|
51
|
+
sources_list = get_debian13_sources_list().rstrip("\n")
|
|
52
|
+
indented_sources = "\n".join(f" {line}" for line in sources_list.split("\n"))
|
|
53
|
+
|
|
54
|
+
config_parts = [
|
|
55
|
+
"#cloud-config",
|
|
56
|
+
"# Generated cloud-init configuration for OTS infrastructure",
|
|
57
|
+
"# Debian 13 (Trixie) with DEB822-style apt sources",
|
|
58
|
+
"",
|
|
59
|
+
"package_update: true",
|
|
60
|
+
"package_upgrade: true",
|
|
61
|
+
"package_reboot_if_required: true",
|
|
62
|
+
"",
|
|
63
|
+
"apt:",
|
|
64
|
+
" sources_list: |",
|
|
65
|
+
indented_sources,
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Add third-party repositories if requested
|
|
69
|
+
sources = []
|
|
70
|
+
|
|
71
|
+
if include_postgresql:
|
|
72
|
+
postgresql_source = {
|
|
73
|
+
"source": "deb http://apt.postgresql.org/pub/repos/apt trixie-pgdg main",
|
|
74
|
+
}
|
|
75
|
+
if postgresql_gpg_key:
|
|
76
|
+
postgresql_source["key"] = postgresql_gpg_key
|
|
77
|
+
else:
|
|
78
|
+
postgresql_source["key"] = "# PostgreSQL GPG key placeholder - replace with actual key"
|
|
79
|
+
sources.append(("postgresql", postgresql_source))
|
|
80
|
+
|
|
81
|
+
if include_valkey:
|
|
82
|
+
valkey_source = {
|
|
83
|
+
"source": "deb https://packages.valkey.io/deb/ trixie main",
|
|
84
|
+
}
|
|
85
|
+
if valkey_gpg_key:
|
|
86
|
+
valkey_source["key"] = valkey_gpg_key
|
|
87
|
+
else:
|
|
88
|
+
valkey_source["key"] = "# Valkey GPG key placeholder - replace with actual key"
|
|
89
|
+
sources.append(("valkey", valkey_source))
|
|
90
|
+
|
|
91
|
+
# Add sources section if we have any
|
|
92
|
+
if sources:
|
|
93
|
+
config_parts.append(" sources:")
|
|
94
|
+
for name, source_config in sources:
|
|
95
|
+
config_parts.append(f" {name}:")
|
|
96
|
+
config_parts.append(f' source: "{source_config["source"]}"')
|
|
97
|
+
if "key" in source_config:
|
|
98
|
+
# Multi-line key handling
|
|
99
|
+
key_content = source_config["key"]
|
|
100
|
+
if "\n" in key_content:
|
|
101
|
+
config_parts.append(" key: |")
|
|
102
|
+
for line in key_content.split("\n"):
|
|
103
|
+
config_parts.append(f" {line}")
|
|
104
|
+
else:
|
|
105
|
+
config_parts.append(f' key: "{key_content}"')
|
|
106
|
+
|
|
107
|
+
# Add common packages section
|
|
108
|
+
config_parts.extend(
|
|
109
|
+
[
|
|
110
|
+
"",
|
|
111
|
+
"packages:",
|
|
112
|
+
" - curl",
|
|
113
|
+
" - wget",
|
|
114
|
+
" - git",
|
|
115
|
+
" - vim",
|
|
116
|
+
" - podman",
|
|
117
|
+
" - systemd-container",
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if include_postgresql:
|
|
122
|
+
config_parts.append(" - postgresql-client")
|
|
123
|
+
|
|
124
|
+
if include_valkey:
|
|
125
|
+
config_parts.append(" - valkey")
|
|
126
|
+
|
|
127
|
+
return "\n".join(config_parts) + "\n"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# src/ots_containers/commands/common.py
|
|
2
|
+
"""Shared CLI annotations for consistency across commands.
|
|
3
|
+
|
|
4
|
+
All common flags use long+short forms for consistency:
|
|
5
|
+
--quiet, -q
|
|
6
|
+
--dry-run, -n
|
|
7
|
+
--yes, -y
|
|
8
|
+
--follow, -f
|
|
9
|
+
--lines, -n
|
|
10
|
+
--json, -j
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import cyclopts
|
|
16
|
+
|
|
17
|
+
# Output control
|
|
18
|
+
Quiet = Annotated[
|
|
19
|
+
bool,
|
|
20
|
+
cyclopts.Parameter(
|
|
21
|
+
name=["--quiet", "-q"],
|
|
22
|
+
help="Suppress output",
|
|
23
|
+
),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
DryRun = Annotated[
|
|
27
|
+
bool,
|
|
28
|
+
cyclopts.Parameter(
|
|
29
|
+
name=["--dry-run", "-n"],
|
|
30
|
+
help="Show what would be done without doing it",
|
|
31
|
+
negative=[], # Disable --no-dry-run generation
|
|
32
|
+
),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Confirmation
|
|
37
|
+
Yes = Annotated[
|
|
38
|
+
bool,
|
|
39
|
+
cyclopts.Parameter(
|
|
40
|
+
name=["--yes", "-y"],
|
|
41
|
+
help="Skip confirmation prompts",
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Log viewing
|
|
47
|
+
Follow = Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
cyclopts.Parameter(
|
|
50
|
+
name=["--follow", "-f"],
|
|
51
|
+
help="Follow log output",
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
Lines = Annotated[
|
|
56
|
+
int,
|
|
57
|
+
cyclopts.Parameter(
|
|
58
|
+
name=["--lines", "-n"],
|
|
59
|
+
help="Number of lines to show",
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# JSON output
|
|
65
|
+
JsonOutput = Annotated[
|
|
66
|
+
bool,
|
|
67
|
+
cyclopts.Parameter(
|
|
68
|
+
name=["--json", "-j"],
|
|
69
|
+
help="Output as JSON",
|
|
70
|
+
),
|
|
71
|
+
]
|