ots-containers 0.3.0__py3-none-any.whl → 0.3.1__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 -1
- ots_containers/commands/README.md +21 -17
- ots_containers/commands/assets.py +1 -0
- 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 +6 -1
- ots_containers/commands/instance/__init__.py +4 -0
- ots_containers/commands/instance/_helpers.py +17 -22
- ots_containers/commands/instance/annotations.py +1 -0
- ots_containers/commands/instance/app.py +418 -40
- 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/db.py +35 -21
- ots_containers/quadlet.py +10 -7
- ots_containers/systemd.py +72 -3
- {ots_containers-0.3.0.dist-info → ots_containers-0.3.1.dist-info}/METADATA +6 -5
- ots_containers-0.3.1.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.1.dist-info}/WHEEL +0 -0
- {ots_containers-0.3.0.dist-info → ots_containers-0.3.1.dist-info}/entry_points.txt +0 -0
- {ots_containers-0.3.0.dist-info → ots_containers-0.3.1.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/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.
|
|
@@ -157,7 +158,11 @@ def init(
|
|
|
157
158
|
print("\nSystem Configuration:")
|
|
158
159
|
quadlet_dir = cfg.web_template_path.parent
|
|
159
160
|
users_dir = quadlet_dir / "users"
|
|
160
|
-
template_paths = [
|
|
161
|
+
template_paths = [
|
|
162
|
+
cfg.web_template_path,
|
|
163
|
+
cfg.worker_template_path,
|
|
164
|
+
cfg.scheduler_template_path,
|
|
165
|
+
]
|
|
161
166
|
if check:
|
|
162
167
|
for template_path in template_paths:
|
|
163
168
|
if template_path.exists():
|
|
@@ -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
|
|
|
@@ -19,25 +22,26 @@ def format_command(cmd: Sequence[str]) -> str:
|
|
|
19
22
|
return " ".join(shlex.quote(arg) for arg in cmd)
|
|
20
23
|
|
|
21
24
|
|
|
22
|
-
def
|
|
23
|
-
"""
|
|
25
|
+
def build_secret_args(env_file: Path) -> list[str]:
|
|
26
|
+
"""Build podman --secret arguments from environment file.
|
|
27
|
+
|
|
28
|
+
Reads SECRET_VARIABLE_NAMES from the env file and generates
|
|
29
|
+
corresponding --secret flags for podman run.
|
|
24
30
|
|
|
25
31
|
Args:
|
|
26
|
-
|
|
32
|
+
env_file: Path to environment file (e.g., /etc/default/onetimesecret)
|
|
27
33
|
|
|
28
34
|
Returns:
|
|
29
|
-
|
|
35
|
+
List of command arguments: ["--secret", "name,type=env,target=VAR", ...]
|
|
30
36
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for id_ in ids:
|
|
34
|
-
tags.append(f"onetime-{itype.value}-{id_}")
|
|
35
|
-
|
|
36
|
-
if not tags:
|
|
37
|
-
return ""
|
|
37
|
+
if not env_file.exists():
|
|
38
|
+
return []
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
secret_specs = get_secrets_from_env_file(env_file)
|
|
41
|
+
args: list[str] = []
|
|
42
|
+
for spec in secret_specs:
|
|
43
|
+
args.extend(["--secret", f"{spec.secret_name},type=env,target={spec.env_var_name}"])
|
|
44
|
+
return args
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
def resolve_identifiers(
|
|
@@ -117,8 +121,6 @@ def for_each_instance(
|
|
|
117
121
|
delay: int,
|
|
118
122
|
action: Callable[[InstanceType, str], None],
|
|
119
123
|
verb: str,
|
|
120
|
-
*,
|
|
121
|
-
show_logs_hint: bool = False,
|
|
122
124
|
) -> int:
|
|
123
125
|
"""Run action for each instance with delay between.
|
|
124
126
|
|
|
@@ -127,7 +129,6 @@ def for_each_instance(
|
|
|
127
129
|
delay: Seconds to wait between operations
|
|
128
130
|
action: Callable taking (instance_type, identifier)
|
|
129
131
|
verb: Present participle for logging (e.g., "Restarting")
|
|
130
|
-
show_logs_hint: If True, print journalctl command to view logs
|
|
131
132
|
|
|
132
133
|
Returns:
|
|
133
134
|
Total number of instances processed.
|
|
@@ -152,10 +153,4 @@ def for_each_instance(
|
|
|
152
153
|
time.sleep(delay)
|
|
153
154
|
|
|
154
155
|
print(f"Processed {total} instance(s)")
|
|
155
|
-
|
|
156
|
-
if show_logs_hint:
|
|
157
|
-
hint = format_journalctl_hint(instances)
|
|
158
|
-
if hint:
|
|
159
|
-
print(f"\nView logs: {hint}")
|
|
160
|
-
|
|
161
156
|
return total
|