ots-containers 0.3.0__py3-none-any.whl → 0.3.2__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 +6 -1
- ots_containers/commands/README.md +21 -17
- ots_containers/commands/assets.py +1 -1
- ots_containers/commands/cloudinit/app.py +5 -1
- ots_containers/commands/cloudinit/templates.py +1 -0
- ots_containers/commands/common.py +1 -0
- ots_containers/commands/env/app.py +5 -1
- ots_containers/commands/image/app.py +35 -16
- ots_containers/commands/init.py +24 -17
- ots_containers/commands/instance/__init__.py +4 -0
- ots_containers/commands/instance/_helpers.py +26 -1
- ots_containers/commands/instance/annotations.py +3 -2
- ots_containers/commands/instance/app.py +457 -38
- ots_containers/commands/proxy/_helpers.py +1 -0
- ots_containers/commands/proxy/app.py +7 -1
- ots_containers/commands/service/_helpers.py +1 -0
- ots_containers/commands/service/app.py +17 -2
- ots_containers/commands/service/packages.py +1 -0
- ots_containers/config.py +20 -7
- ots_containers/db.py +35 -21
- ots_containers/quadlet.py +39 -13
- ots_containers/systemd.py +72 -3
- {ots_containers-0.3.0.dist-info → ots_containers-0.3.2.dist-info}/METADATA +8 -7
- ots_containers-0.3.2.dist-info/RECORD +37 -0
- ots_containers-0.3.0.dist-info/RECORD +0 -37
- {ots_containers-0.3.0.dist-info → ots_containers-0.3.2.dist-info}/WHEEL +0 -0
- {ots_containers-0.3.0.dist-info → ots_containers-0.3.2.dist-info}/entry_points.txt +0 -0
- {ots_containers-0.3.0.dist-info → ots_containers-0.3.2.dist-info}/licenses/LICENSE +0 -0
ots_containers/__init__.py
CHANGED
|
@@ -12,10 +12,10 @@ ots-containers <topic> <command> [identifiers] [flags]
|
|
|
12
12
|
|
|
13
13
|
Three container types, each with explicit naming:
|
|
14
14
|
|
|
15
|
-
| Type
|
|
16
|
-
|
|
17
|
-
| `web`
|
|
18
|
-
| `worker`
|
|
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
19
|
| `scheduler` | `onetime-scheduler@{id}` | Name or number | Scheduled tasks |
|
|
20
20
|
|
|
21
21
|
## Command Syntax
|
|
@@ -39,14 +39,14 @@ ots instances logs --scheduler -f # only scheduler logs
|
|
|
39
39
|
|
|
40
40
|
Each topic is a separate module with its own `cyclopts.App`:
|
|
41
41
|
|
|
42
|
-
| Topic
|
|
43
|
-
|
|
44
|
-
| `instance`
|
|
45
|
-
| `service`
|
|
46
|
-
| `image`
|
|
47
|
-
| `assets`
|
|
48
|
-
| `cloudinit` | Cloud-init configuration generation
|
|
49
|
-
| `env`
|
|
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
50
|
|
|
51
51
|
To add a new topic, create a module and register it in `cli.py`.
|
|
52
52
|
|
|
@@ -55,24 +55,28 @@ To add a new topic, create a module and register it in `cli.py`.
|
|
|
55
55
|
Commands are categorized by their impact:
|
|
56
56
|
|
|
57
57
|
### High-level (affects config + state)
|
|
58
|
+
|
|
58
59
|
Commands that modify quadlet templates, database records, or both:
|
|
60
|
+
|
|
59
61
|
- `deploy`, `redeploy`, `undeploy`
|
|
60
62
|
|
|
61
63
|
These commands should document their config impact in the docstring.
|
|
62
64
|
|
|
63
65
|
### Low-level (runtime control only)
|
|
66
|
+
|
|
64
67
|
Commands that only interact with systemd, no config changes:
|
|
68
|
+
|
|
65
69
|
- `start`, `stop`, `restart`, `status`, `logs`, `enable`, `disable`, `exec`
|
|
66
70
|
|
|
67
71
|
These commands should explicitly state they do NOT refresh config.
|
|
68
72
|
|
|
69
73
|
## Naming Conventions
|
|
70
74
|
|
|
71
|
-
| Pattern
|
|
72
|
-
|
|
73
|
-
| Verb
|
|
74
|
-
| `--flag`
|
|
75
|
-
| `--option VALUE`
|
|
75
|
+
| Pattern | Example | Use for |
|
|
76
|
+
| ------------------ | ---------------------------------- | ----------------------- |
|
|
77
|
+
| Verb | `deploy`, `sync` | Actions |
|
|
78
|
+
| `--flag` | `--force`, `--yes` | Boolean options |
|
|
79
|
+
| `--option VALUE` | `--delay 5`, `--lines 50` | Value options |
|
|
76
80
|
| `--type` shortcuts | `--web`, `--worker`, `--scheduler` | Instance type selection |
|
|
77
81
|
|
|
78
82
|
## Default Commands
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# src/ots_containers/commands/assets.py
|
|
2
|
+
|
|
2
3
|
"""Asset management commands for OTS containers."""
|
|
3
4
|
|
|
4
5
|
from typing import Annotated
|
|
@@ -25,5 +26,4 @@ def sync(
|
|
|
25
26
|
on initial setup.
|
|
26
27
|
"""
|
|
27
28
|
cfg = Config()
|
|
28
|
-
cfg.validate()
|
|
29
29
|
assets_module.update(cfg, create_volume=create_volume)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# src/ots_containers/commands/cloudinit/app.py
|
|
2
|
+
|
|
2
3
|
"""Cloud-init configuration generation commands.
|
|
3
4
|
|
|
4
5
|
Generates cloud-init YAML with Debian 13 (Trixie) DEB822-style apt sources.
|
|
@@ -74,7 +75,10 @@ def generate(
|
|
|
74
75
|
"Warning: --include-postgresql specified but no --postgresql-key provided",
|
|
75
76
|
file=sys.stderr,
|
|
76
77
|
)
|
|
77
|
-
print(
|
|
78
|
+
print(
|
|
79
|
+
"PostgreSQL repository will use inline key placeholder",
|
|
80
|
+
file=sys.stderr,
|
|
81
|
+
)
|
|
78
82
|
|
|
79
83
|
if include_valkey and valkey_key:
|
|
80
84
|
valkey_gpg = Path(valkey_key).read_text()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# src/ots_containers/commands/env/app.py
|
|
2
|
+
|
|
2
3
|
"""Environment file management commands.
|
|
3
4
|
|
|
4
5
|
Process environment files to extract secrets and prepare for container deployment.
|
|
@@ -274,7 +275,10 @@ def quadlet_lines(
|
|
|
274
275
|
parsed = EnvFile.parse(path)
|
|
275
276
|
|
|
276
277
|
if not parsed.secret_variable_names:
|
|
277
|
-
print(
|
|
278
|
+
print(
|
|
279
|
+
"Error: No SECRET_VARIABLE_NAMES defined in environment file.",
|
|
280
|
+
file=sys.stderr,
|
|
281
|
+
)
|
|
278
282
|
return 1
|
|
279
283
|
|
|
280
284
|
secrets, messages = extract_secrets(parsed)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# src/ots_containers/commands/image/app.py
|
|
2
|
+
|
|
2
3
|
"""Image management commands for OTS containers.
|
|
3
4
|
|
|
4
5
|
Supports pulling from multiple registries:
|
|
@@ -33,12 +34,12 @@ app = cyclopts.App(
|
|
|
33
34
|
@app.command
|
|
34
35
|
def pull(
|
|
35
36
|
tag: Annotated[
|
|
36
|
-
str,
|
|
37
|
+
str | None,
|
|
37
38
|
cyclopts.Parameter(
|
|
38
39
|
name=["--tag", "-t"],
|
|
39
|
-
help="Image tag to pull (
|
|
40
|
+
help="Image tag to pull (default: from TAG env var)",
|
|
40
41
|
),
|
|
41
|
-
],
|
|
42
|
+
] = None,
|
|
42
43
|
image: Annotated[
|
|
43
44
|
str,
|
|
44
45
|
cyclopts.Parameter(
|
|
@@ -60,6 +61,13 @@ def pull(
|
|
|
60
61
|
help="Pull from private registry (uses configured OTS_REGISTRY)",
|
|
61
62
|
),
|
|
62
63
|
] = False,
|
|
64
|
+
platform: Annotated[
|
|
65
|
+
str | None,
|
|
66
|
+
cyclopts.Parameter(
|
|
67
|
+
name=["--platform", "-p"],
|
|
68
|
+
help="Target platform (e.g., linux/amd64, linux/arm64)",
|
|
69
|
+
),
|
|
70
|
+
] = None,
|
|
63
71
|
quiet: Quiet = False,
|
|
64
72
|
):
|
|
65
73
|
"""Pull a container image from registry.
|
|
@@ -67,11 +75,19 @@ def pull(
|
|
|
67
75
|
Examples:
|
|
68
76
|
ots image pull --tag v0.23.0
|
|
69
77
|
ots image pull --tag latest --current
|
|
78
|
+
TAG=dev ots image pull # Use TAG env var
|
|
70
79
|
ots image pull --tag v0.23.0 --image docker.io/onetimesecret/onetimesecret
|
|
71
80
|
ots image pull --tag v0.23.0 --private # Pull from private registry
|
|
81
|
+
ots image pull --tag dev --platform linux/amd64 # Pull amd64 on Apple Silicon
|
|
72
82
|
"""
|
|
73
83
|
cfg = Config()
|
|
74
84
|
|
|
85
|
+
# Resolve tag from env var if not provided
|
|
86
|
+
resolved_tag = tag or cfg.tag
|
|
87
|
+
if not resolved_tag:
|
|
88
|
+
print("Error: --tag is required (or set TAG env var)")
|
|
89
|
+
raise SystemExit(1)
|
|
90
|
+
|
|
75
91
|
# Use private registry if requested
|
|
76
92
|
if private:
|
|
77
93
|
if not cfg.private_image:
|
|
@@ -79,20 +95,23 @@ def pull(
|
|
|
79
95
|
raise SystemExit(1)
|
|
80
96
|
image = cfg.private_image
|
|
81
97
|
|
|
82
|
-
full_image = f"{image}:{
|
|
98
|
+
full_image = f"{image}:{resolved_tag}"
|
|
83
99
|
|
|
84
100
|
if not quiet:
|
|
85
101
|
print(f"Pulling {full_image}...")
|
|
86
102
|
|
|
87
103
|
try:
|
|
88
104
|
# Use auth file for authenticated registries
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
pull_kwargs = {
|
|
106
|
+
"authfile": str(cfg.registry_auth_file),
|
|
107
|
+
"check": True,
|
|
108
|
+
"capture_output": True,
|
|
109
|
+
"text": True,
|
|
110
|
+
}
|
|
111
|
+
if platform:
|
|
112
|
+
pull_kwargs["platform"] = platform
|
|
113
|
+
|
|
114
|
+
podman.pull(full_image, **pull_kwargs)
|
|
96
115
|
if not quiet:
|
|
97
116
|
print(f"Successfully pulled {full_image}")
|
|
98
117
|
except Exception as e:
|
|
@@ -100,7 +119,7 @@ def pull(
|
|
|
100
119
|
db.record_deployment(
|
|
101
120
|
cfg.db_path,
|
|
102
121
|
image=image,
|
|
103
|
-
tag=
|
|
122
|
+
tag=resolved_tag,
|
|
104
123
|
action="pull",
|
|
105
124
|
success=False,
|
|
106
125
|
notes=str(e),
|
|
@@ -111,19 +130,19 @@ def pull(
|
|
|
111
130
|
db.record_deployment(
|
|
112
131
|
cfg.db_path,
|
|
113
132
|
image=image,
|
|
114
|
-
tag=
|
|
133
|
+
tag=resolved_tag,
|
|
115
134
|
action="pull",
|
|
116
135
|
success=True,
|
|
117
136
|
)
|
|
118
137
|
|
|
119
138
|
# Set as current if requested
|
|
120
139
|
if set_as_current:
|
|
121
|
-
previous = db.set_current(cfg.db_path, image,
|
|
140
|
+
previous = db.set_current(cfg.db_path, image, resolved_tag)
|
|
122
141
|
if not quiet:
|
|
123
142
|
if previous:
|
|
124
|
-
print(f"Set CURRENT to {
|
|
143
|
+
print(f"Set CURRENT to {resolved_tag} (previous: {previous})")
|
|
125
144
|
else:
|
|
126
|
-
print(f"Set CURRENT to {
|
|
145
|
+
print(f"Set CURRENT to {resolved_tag}")
|
|
127
146
|
|
|
128
147
|
|
|
129
148
|
@app.default
|
ots_containers/commands/init.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# src/ots_containers/commands/init.py
|
|
2
|
+
|
|
2
3
|
"""Init command for idempotent setup of ots-containers.
|
|
3
4
|
|
|
4
5
|
Creates required directories and initializes the deployment database.
|
|
@@ -13,7 +14,7 @@ from typing import Annotated
|
|
|
13
14
|
import cyclopts
|
|
14
15
|
|
|
15
16
|
from ots_containers import db
|
|
16
|
-
from ots_containers.config import Config
|
|
17
|
+
from ots_containers.config import CONFIG_FILES, Config
|
|
17
18
|
|
|
18
19
|
app = cyclopts.App(
|
|
19
20
|
name="init",
|
|
@@ -125,16 +126,17 @@ def init(
|
|
|
125
126
|
prefix = "Re-initializing" if is_reinit else "Initializing"
|
|
126
127
|
print(f"{prefix} ots-containers...")
|
|
127
128
|
|
|
128
|
-
# 1. App Configuration - user-managed config files
|
|
129
|
+
# 1. App Configuration - user-managed config files (all optional)
|
|
129
130
|
# Note: /etc/default/onetimesecret and Podman secrets are managed separately
|
|
130
131
|
if not quiet or check:
|
|
131
132
|
print("\nApp Configuration:")
|
|
132
133
|
if check:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
for fname in CONFIG_FILES:
|
|
135
|
+
fpath = cfg.config_dir / fname
|
|
136
|
+
if fpath.exists():
|
|
137
|
+
print(f" [ok] {fpath}")
|
|
138
|
+
else:
|
|
139
|
+
print(f" [optional] {fpath}")
|
|
138
140
|
else:
|
|
139
141
|
result = _create_directory(cfg.config_dir, mode=0o755, quiet=True)
|
|
140
142
|
if result is None:
|
|
@@ -143,21 +145,27 @@ def init(
|
|
|
143
145
|
# Directory exists or was created - now handle config files
|
|
144
146
|
if source_dir:
|
|
145
147
|
src = Path(source_dir)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
for fname in CONFIG_FILES:
|
|
149
|
+
if _copy_template(src / fname, cfg.config_dir / fname, quiet=quiet) is None:
|
|
150
|
+
all_ok = False
|
|
148
151
|
elif not quiet:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
for fname in CONFIG_FILES:
|
|
153
|
+
fpath = cfg.config_dir / fname
|
|
154
|
+
if fpath.exists():
|
|
155
|
+
print(f" [ok] {fpath}")
|
|
156
|
+
else:
|
|
157
|
+
print(f" [optional] {fpath}")
|
|
154
158
|
|
|
155
159
|
# 2. System Configuration - quadlet files
|
|
156
160
|
if not quiet or check:
|
|
157
161
|
print("\nSystem Configuration:")
|
|
158
162
|
quadlet_dir = cfg.web_template_path.parent
|
|
159
163
|
users_dir = quadlet_dir / "users"
|
|
160
|
-
template_paths = [
|
|
164
|
+
template_paths = [
|
|
165
|
+
cfg.web_template_path,
|
|
166
|
+
cfg.worker_template_path,
|
|
167
|
+
cfg.scheduler_template_path,
|
|
168
|
+
]
|
|
161
169
|
if check:
|
|
162
170
|
for template_path in template_paths:
|
|
163
171
|
if template_path.exists():
|
|
@@ -243,8 +251,7 @@ def init(
|
|
|
243
251
|
print("\nInitialization incomplete - some operations failed.")
|
|
244
252
|
print("Try running with elevated privileges: sudo ots init")
|
|
245
253
|
print("\nNext steps:")
|
|
246
|
-
|
|
247
|
-
print(f" 1. Create {cfg.config_yaml}")
|
|
254
|
+
print(f" 1. (Optional) Place config overrides in {cfg.config_dir}/")
|
|
248
255
|
print(" 2. Create /etc/default/onetimesecret with infrastructure env vars")
|
|
249
256
|
print(" 3. Create Podman secrets: ots_hmac_secret, ots_secret, ots_session_secret")
|
|
250
257
|
print(" 4. Run 'ots image pull --tag <version>' to pull an image")
|
|
@@ -14,6 +14,7 @@ from .annotations import (
|
|
|
14
14
|
)
|
|
15
15
|
from .app import (
|
|
16
16
|
app,
|
|
17
|
+
config_transform,
|
|
17
18
|
deploy,
|
|
18
19
|
disable,
|
|
19
20
|
enable,
|
|
@@ -23,6 +24,7 @@ from .app import (
|
|
|
23
24
|
redeploy,
|
|
24
25
|
restart,
|
|
25
26
|
run,
|
|
27
|
+
shell,
|
|
26
28
|
show_env,
|
|
27
29
|
start,
|
|
28
30
|
status,
|
|
@@ -39,6 +41,7 @@ __all__ = [
|
|
|
39
41
|
"WebFlag",
|
|
40
42
|
"WorkerFlag",
|
|
41
43
|
"app",
|
|
44
|
+
"config_transform",
|
|
42
45
|
"deploy",
|
|
43
46
|
"disable",
|
|
44
47
|
"enable",
|
|
@@ -49,6 +52,7 @@ __all__ = [
|
|
|
49
52
|
"resolve_instance_type",
|
|
50
53
|
"restart",
|
|
51
54
|
"run",
|
|
55
|
+
"shell",
|
|
52
56
|
"show_env",
|
|
53
57
|
"start",
|
|
54
58
|
"status",
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# src/ots_containers/commands/instance/_helpers.py
|
|
2
|
+
|
|
2
3
|
"""Internal helper functions for instance commands."""
|
|
3
4
|
|
|
4
5
|
import shlex
|
|
5
6
|
import time
|
|
6
7
|
from collections.abc import Callable, Sequence
|
|
8
|
+
from pathlib import Path
|
|
7
9
|
|
|
8
10
|
from ots_containers import systemd
|
|
11
|
+
from ots_containers.environment_file import get_secrets_from_env_file
|
|
9
12
|
|
|
10
13
|
from .annotations import InstanceType
|
|
11
14
|
|
|
@@ -36,10 +39,32 @@ def format_journalctl_hint(instances: dict[InstanceType, list[str]]) -> str:
|
|
|
36
39
|
if not tags:
|
|
37
40
|
return ""
|
|
38
41
|
|
|
39
|
-
tag_args = " ".join(f"-t {tag}" for tag in tags)
|
|
42
|
+
tag_args = " ".join(f"-t {shlex.quote(tag)}" for tag in tags)
|
|
40
43
|
return f"journalctl {tag_args} -f"
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
def build_secret_args(env_file: Path) -> list[str]:
|
|
47
|
+
"""Build podman --secret arguments from environment file.
|
|
48
|
+
|
|
49
|
+
Reads SECRET_VARIABLE_NAMES from the env file and generates
|
|
50
|
+
corresponding --secret flags for podman run.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
env_file: Path to environment file (e.g., /etc/default/onetimesecret)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of command arguments: ["--secret", "name,type=env,target=VAR", ...]
|
|
57
|
+
"""
|
|
58
|
+
if not env_file.exists():
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
secret_specs = get_secrets_from_env_file(env_file)
|
|
62
|
+
args: list[str] = []
|
|
63
|
+
for spec in secret_specs:
|
|
64
|
+
args.extend(["--secret", f"{spec.secret_name},type=env,target={spec.env_var_name}"])
|
|
65
|
+
return args
|
|
66
|
+
|
|
67
|
+
|
|
43
68
|
def resolve_identifiers(
|
|
44
69
|
identifiers: tuple[str, ...],
|
|
45
70
|
instance_type: InstanceType | None,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# src/ots_containers/commands/instance/annotations.py
|
|
2
|
+
|
|
2
3
|
"""Type annotations for instance commands.
|
|
3
4
|
|
|
4
5
|
Instance types:
|
|
@@ -12,13 +13,13 @@ Identifier patterns:
|
|
|
12
13
|
- Scheduler: numeric or named (main, 1)
|
|
13
14
|
"""
|
|
14
15
|
|
|
15
|
-
from enum import
|
|
16
|
+
from enum import StrEnum
|
|
16
17
|
from typing import Annotated
|
|
17
18
|
|
|
18
19
|
import cyclopts
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
class InstanceType(
|
|
22
|
+
class InstanceType(StrEnum):
|
|
22
23
|
"""Type of container instance."""
|
|
23
24
|
|
|
24
25
|
WEB = "web"
|