ots-containers 0.3.1__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 +5 -2
- ots_containers/commands/assets.py +0 -1
- ots_containers/commands/init.py +18 -16
- ots_containers/commands/instance/_helpers.py +30 -0
- ots_containers/commands/instance/annotations.py +2 -2
- ots_containers/commands/instance/app.py +61 -20
- ots_containers/config.py +20 -7
- ots_containers/quadlet.py +41 -18
- {ots_containers-0.3.1.dist-info → ots_containers-0.3.2.dist-info}/METADATA +3 -3
- {ots_containers-0.3.1.dist-info → ots_containers-0.3.2.dist-info}/RECORD +13 -13
- {ots_containers-0.3.1.dist-info → ots_containers-0.3.2.dist-info}/WHEEL +0 -0
- {ots_containers-0.3.1.dist-info → ots_containers-0.3.2.dist-info}/entry_points.txt +0 -0
- {ots_containers-0.3.1.dist-info → ots_containers-0.3.2.dist-info}/licenses/LICENSE +0 -0
ots_containers/__init__.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# src/ots_containers/__init__.py
|
|
2
2
|
|
|
3
|
-
from importlib.metadata import version
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("ots-containers")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0.0.0+dev"
|
ots_containers/commands/init.py
CHANGED
|
@@ -14,7 +14,7 @@ from typing import Annotated
|
|
|
14
14
|
import cyclopts
|
|
15
15
|
|
|
16
16
|
from ots_containers import db
|
|
17
|
-
from ots_containers.config import Config
|
|
17
|
+
from ots_containers.config import CONFIG_FILES, Config
|
|
18
18
|
|
|
19
19
|
app = cyclopts.App(
|
|
20
20
|
name="init",
|
|
@@ -126,16 +126,17 @@ def init(
|
|
|
126
126
|
prefix = "Re-initializing" if is_reinit else "Initializing"
|
|
127
127
|
print(f"{prefix} ots-containers...")
|
|
128
128
|
|
|
129
|
-
# 1. App Configuration - user-managed config files
|
|
129
|
+
# 1. App Configuration - user-managed config files (all optional)
|
|
130
130
|
# Note: /etc/default/onetimesecret and Podman secrets are managed separately
|
|
131
131
|
if not quiet or check:
|
|
132
132
|
print("\nApp Configuration:")
|
|
133
133
|
if check:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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}")
|
|
139
140
|
else:
|
|
140
141
|
result = _create_directory(cfg.config_dir, mode=0o755, quiet=True)
|
|
141
142
|
if result is None:
|
|
@@ -144,14 +145,16 @@ def init(
|
|
|
144
145
|
# Directory exists or was created - now handle config files
|
|
145
146
|
if source_dir:
|
|
146
147
|
src = Path(source_dir)
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
for fname in CONFIG_FILES:
|
|
149
|
+
if _copy_template(src / fname, cfg.config_dir / fname, quiet=quiet) is None:
|
|
150
|
+
all_ok = False
|
|
149
151
|
elif not quiet:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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}")
|
|
155
158
|
|
|
156
159
|
# 2. System Configuration - quadlet files
|
|
157
160
|
if not quiet or check:
|
|
@@ -248,8 +251,7 @@ def init(
|
|
|
248
251
|
print("\nInitialization incomplete - some operations failed.")
|
|
249
252
|
print("Try running with elevated privileges: sudo ots init")
|
|
250
253
|
print("\nNext steps:")
|
|
251
|
-
|
|
252
|
-
print(f" 1. Create {cfg.config_yaml}")
|
|
254
|
+
print(f" 1. (Optional) Place config overrides in {cfg.config_dir}/")
|
|
253
255
|
print(" 2. Create /etc/default/onetimesecret with infrastructure env vars")
|
|
254
256
|
print(" 3. Create Podman secrets: ots_hmac_secret, ots_secret, ots_session_secret")
|
|
255
257
|
print(" 4. Run 'ots image pull --tag <version>' to pull an image")
|
|
@@ -22,6 +22,27 @@ def format_command(cmd: Sequence[str]) -> str:
|
|
|
22
22
|
return " ".join(shlex.quote(arg) for arg in cmd)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def format_journalctl_hint(instances: dict[InstanceType, list[str]]) -> str:
|
|
26
|
+
"""Generate a journalctl command to view logs for the given instances.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
instances: Dict mapping InstanceType to list of identifiers
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A journalctl command string with -t flags for each instance.
|
|
33
|
+
"""
|
|
34
|
+
tags = []
|
|
35
|
+
for itype, ids in instances.items():
|
|
36
|
+
for id_ in ids:
|
|
37
|
+
tags.append(f"onetime-{itype.value}-{id_}")
|
|
38
|
+
|
|
39
|
+
if not tags:
|
|
40
|
+
return ""
|
|
41
|
+
|
|
42
|
+
tag_args = " ".join(f"-t {shlex.quote(tag)}" for tag in tags)
|
|
43
|
+
return f"journalctl {tag_args} -f"
|
|
44
|
+
|
|
45
|
+
|
|
25
46
|
def build_secret_args(env_file: Path) -> list[str]:
|
|
26
47
|
"""Build podman --secret arguments from environment file.
|
|
27
48
|
|
|
@@ -121,6 +142,8 @@ def for_each_instance(
|
|
|
121
142
|
delay: int,
|
|
122
143
|
action: Callable[[InstanceType, str], None],
|
|
123
144
|
verb: str,
|
|
145
|
+
*,
|
|
146
|
+
show_logs_hint: bool = False,
|
|
124
147
|
) -> int:
|
|
125
148
|
"""Run action for each instance with delay between.
|
|
126
149
|
|
|
@@ -129,6 +152,7 @@ def for_each_instance(
|
|
|
129
152
|
delay: Seconds to wait between operations
|
|
130
153
|
action: Callable taking (instance_type, identifier)
|
|
131
154
|
verb: Present participle for logging (e.g., "Restarting")
|
|
155
|
+
show_logs_hint: If True, print journalctl command to view logs
|
|
132
156
|
|
|
133
157
|
Returns:
|
|
134
158
|
Total number of instances processed.
|
|
@@ -153,4 +177,10 @@ def for_each_instance(
|
|
|
153
177
|
time.sleep(delay)
|
|
154
178
|
|
|
155
179
|
print(f"Processed {total} instance(s)")
|
|
180
|
+
|
|
181
|
+
if show_logs_hint:
|
|
182
|
+
hint = format_journalctl_hint(instances)
|
|
183
|
+
if hint:
|
|
184
|
+
print(f"\nView logs: {hint}")
|
|
185
|
+
|
|
156
186
|
return total
|
|
@@ -13,13 +13,13 @@ Identifier patterns:
|
|
|
13
13
|
- Scheduler: numeric or named (main, 1)
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
from enum import
|
|
16
|
+
from enum import StrEnum
|
|
17
17
|
from typing import Annotated
|
|
18
18
|
|
|
19
19
|
import cyclopts
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class InstanceType(
|
|
22
|
+
class InstanceType(StrEnum):
|
|
23
23
|
"""Type of container instance."""
|
|
24
24
|
|
|
25
25
|
WEB = "web"
|
|
@@ -15,6 +15,7 @@ from ._helpers import (
|
|
|
15
15
|
build_secret_args,
|
|
16
16
|
for_each_instance,
|
|
17
17
|
format_command,
|
|
18
|
+
format_journalctl_hint,
|
|
18
19
|
resolve_identifiers,
|
|
19
20
|
)
|
|
20
21
|
from .annotations import (
|
|
@@ -40,6 +41,7 @@ def _list_instances_impl(
|
|
|
40
41
|
json_output: bool,
|
|
41
42
|
):
|
|
42
43
|
"""Shared implementation for listing instances."""
|
|
44
|
+
systemd.require_systemctl()
|
|
43
45
|
instances = resolve_identifiers(identifiers, instance_type, running_only=False)
|
|
44
46
|
|
|
45
47
|
if not instances:
|
|
@@ -324,8 +326,9 @@ def run(
|
|
|
324
326
|
]
|
|
325
327
|
)
|
|
326
328
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
+
# Config overrides (per-file)
|
|
330
|
+
for f in cfg.existing_config_files:
|
|
331
|
+
cmd.extend(["-v", f"{f}:/app/etc/{f.name}:ro"])
|
|
329
332
|
cmd.extend(["-v", "static_assets:/app/public:ro"])
|
|
330
333
|
|
|
331
334
|
# Auth file for private registry
|
|
@@ -390,13 +393,16 @@ def deploy(
|
|
|
390
393
|
raise SystemExit("Instance type required for deploy. Use --web, --worker, or --scheduler.")
|
|
391
394
|
|
|
392
395
|
cfg = Config()
|
|
393
|
-
cfg.validate()
|
|
394
396
|
|
|
395
397
|
# Resolve image/tag (handles CURRENT/ROLLBACK aliases)
|
|
396
398
|
image, tag = cfg.resolve_image_tag()
|
|
397
399
|
if not quiet:
|
|
398
400
|
print(f"Image: {image}:{tag}")
|
|
399
|
-
|
|
401
|
+
if cfg.has_custom_config:
|
|
402
|
+
mounted = [f.name for f in cfg.existing_config_files]
|
|
403
|
+
print(f"Config overrides: {', '.join(mounted)}")
|
|
404
|
+
else:
|
|
405
|
+
print("Config: using container built-in defaults")
|
|
400
406
|
|
|
401
407
|
if dry_run:
|
|
402
408
|
print(f"[dry-run] Would deploy {itype.value}: {', '.join(identifiers)}")
|
|
@@ -431,6 +437,11 @@ def deploy(
|
|
|
431
437
|
)
|
|
432
438
|
except Exception as e:
|
|
433
439
|
port = int(id_) if inst_type == InstanceType.WEB else 0
|
|
440
|
+
fail_notes = (
|
|
441
|
+
str(e)
|
|
442
|
+
if inst_type == InstanceType.WEB
|
|
443
|
+
else f"{inst_type.value}_id={id_}; error={e}"
|
|
444
|
+
)
|
|
434
445
|
db.record_deployment(
|
|
435
446
|
cfg.db_path,
|
|
436
447
|
image=image,
|
|
@@ -438,12 +449,12 @@ def deploy(
|
|
|
438
449
|
action=f"deploy-{inst_type.value}",
|
|
439
450
|
port=port,
|
|
440
451
|
success=False,
|
|
441
|
-
notes=
|
|
452
|
+
notes=fail_notes,
|
|
442
453
|
)
|
|
443
454
|
raise
|
|
444
455
|
|
|
445
456
|
instances = {itype: list(identifiers)}
|
|
446
|
-
for_each_instance(instances, delay, do_deploy, "Deploying")
|
|
457
|
+
for_each_instance(instances, delay, do_deploy, "Deploying", show_logs_hint=True)
|
|
447
458
|
|
|
448
459
|
|
|
449
460
|
@app.command
|
|
@@ -488,13 +499,16 @@ def redeploy(
|
|
|
488
499
|
return
|
|
489
500
|
|
|
490
501
|
cfg = Config()
|
|
491
|
-
cfg.validate()
|
|
492
502
|
|
|
493
503
|
# Resolve image/tag (handles CURRENT/ROLLBACK aliases)
|
|
494
504
|
image, tag = cfg.resolve_image_tag()
|
|
495
505
|
if not quiet:
|
|
496
506
|
print(f"Image: {image}:{tag}")
|
|
497
|
-
|
|
507
|
+
if cfg.has_custom_config:
|
|
508
|
+
mounted = [f.name for f in cfg.existing_config_files]
|
|
509
|
+
print(f"Config overrides: {', '.join(mounted)}")
|
|
510
|
+
else:
|
|
511
|
+
print("Config: using container built-in defaults")
|
|
498
512
|
|
|
499
513
|
if dry_run:
|
|
500
514
|
verb = "force redeploy" if force else "redeploy"
|
|
@@ -543,6 +557,11 @@ def redeploy(
|
|
|
543
557
|
)
|
|
544
558
|
except Exception as e:
|
|
545
559
|
port = int(id_) if inst_type == InstanceType.WEB else 0
|
|
560
|
+
fail_notes = (
|
|
561
|
+
str(e)
|
|
562
|
+
if inst_type == InstanceType.WEB
|
|
563
|
+
else f"{inst_type.value}_id={id_}; error={e}"
|
|
564
|
+
)
|
|
546
565
|
db.record_deployment(
|
|
547
566
|
cfg.db_path,
|
|
548
567
|
image=image,
|
|
@@ -550,12 +569,12 @@ def redeploy(
|
|
|
550
569
|
action=f"redeploy-{inst_type.value}",
|
|
551
570
|
port=port,
|
|
552
571
|
success=False,
|
|
553
|
-
notes=
|
|
572
|
+
notes=fail_notes,
|
|
554
573
|
)
|
|
555
574
|
raise
|
|
556
575
|
|
|
557
576
|
verb = "Force redeploying" if force else "Redeploying"
|
|
558
|
-
for_each_instance(instances, delay, do_redeploy, verb)
|
|
577
|
+
for_each_instance(instances, delay, do_redeploy, verb, show_logs_hint=True)
|
|
559
578
|
|
|
560
579
|
|
|
561
580
|
@app.command
|
|
@@ -623,6 +642,11 @@ def undeploy(
|
|
|
623
642
|
)
|
|
624
643
|
except Exception as e:
|
|
625
644
|
port = int(id_) if inst_type == InstanceType.WEB else 0
|
|
645
|
+
fail_notes = (
|
|
646
|
+
str(e)
|
|
647
|
+
if inst_type == InstanceType.WEB
|
|
648
|
+
else f"{inst_type.value}_id={id_}; error={e}"
|
|
649
|
+
)
|
|
626
650
|
db.record_deployment(
|
|
627
651
|
cfg.db_path,
|
|
628
652
|
image=image,
|
|
@@ -630,7 +654,7 @@ def undeploy(
|
|
|
630
654
|
action=f"undeploy-{inst_type.value}",
|
|
631
655
|
port=port,
|
|
632
656
|
success=False,
|
|
633
|
-
notes=
|
|
657
|
+
notes=fail_notes,
|
|
634
658
|
)
|
|
635
659
|
raise
|
|
636
660
|
|
|
@@ -668,6 +692,10 @@ def start(
|
|
|
668
692
|
systemd.start(unit)
|
|
669
693
|
print(f"Started {unit}")
|
|
670
694
|
|
|
695
|
+
hint = format_journalctl_hint(instances)
|
|
696
|
+
if hint:
|
|
697
|
+
print(f"\nView logs: {hint}")
|
|
698
|
+
|
|
671
699
|
|
|
672
700
|
@app.command
|
|
673
701
|
def stop(
|
|
@@ -734,6 +762,10 @@ def restart(
|
|
|
734
762
|
systemd.restart(unit)
|
|
735
763
|
print(f"Restarted {unit}")
|
|
736
764
|
|
|
765
|
+
hint = format_journalctl_hint(instances)
|
|
766
|
+
if hint:
|
|
767
|
+
print(f"\nView logs: {hint}")
|
|
768
|
+
|
|
737
769
|
|
|
738
770
|
@app.command
|
|
739
771
|
def enable(
|
|
@@ -1059,10 +1091,10 @@ def shell(
|
|
|
1059
1091
|
else:
|
|
1060
1092
|
cmd.extend(["--tmpfs", "/app/data"])
|
|
1061
1093
|
|
|
1062
|
-
# Config
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1094
|
+
# Config overrides (per-file, if any exist on host)
|
|
1095
|
+
for f in cfg.existing_config_files:
|
|
1096
|
+
resolved = f.resolve() # symlink resolution for macOS podman VM
|
|
1097
|
+
cmd.extend(["-v", f"{resolved}:/app/etc/{f.name}:ro"])
|
|
1066
1098
|
|
|
1067
1099
|
# Image
|
|
1068
1100
|
cmd.append(full_image)
|
|
@@ -1129,9 +1161,14 @@ def config_transform(
|
|
|
1129
1161
|
"""Transform config files with backup/apply workflow.
|
|
1130
1162
|
|
|
1131
1163
|
Runs a migration command in a container to transform config files.
|
|
1132
|
-
By default shows a diff without making changes (dry-run).
|
|
1164
|
+
By default shows a unified diff without making changes (dry-run).
|
|
1133
1165
|
Use --apply to backup the original and apply the transformation.
|
|
1134
1166
|
|
|
1167
|
+
Note: Config files (config.yaml, auth.yaml, logging.yaml) contain
|
|
1168
|
+
application settings, not secrets. Secrets are managed separately
|
|
1169
|
+
via podman secrets and env files. The diff output is the primary
|
|
1170
|
+
interface for reviewing proposed changes before applying them.
|
|
1171
|
+
|
|
1135
1172
|
The migration command should:
|
|
1136
1173
|
- Read from /app/data/{file} (original config copied there)
|
|
1137
1174
|
- Write to /app/data/{file}.new (transformed output)
|
|
@@ -1230,6 +1267,9 @@ def config_transform(
|
|
|
1230
1267
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
1231
1268
|
|
|
1232
1269
|
if result.returncode != 0:
|
|
1270
|
+
# Show migration command output for operator debugging.
|
|
1271
|
+
# No secrets here: env vars are passed via podman secrets,
|
|
1272
|
+
# not visible in command stdout/stderr.
|
|
1233
1273
|
print(f"Migration command failed (exit {result.returncode})")
|
|
1234
1274
|
if result.stderr:
|
|
1235
1275
|
print(result.stderr)
|
|
@@ -1261,8 +1301,9 @@ def config_transform(
|
|
|
1261
1301
|
new_content = read_result.stdout
|
|
1262
1302
|
original_content = config_path.read_text()
|
|
1263
1303
|
|
|
1264
|
-
#
|
|
1265
|
-
|
|
1304
|
+
# Show unified diff of proposed config changes. This is the primary
|
|
1305
|
+
# output of dry-run mode — config files contain app settings, not secrets.
|
|
1306
|
+
config_diff = list(
|
|
1266
1307
|
difflib.unified_diff(
|
|
1267
1308
|
original_content.splitlines(keepends=True),
|
|
1268
1309
|
new_content.splitlines(keepends=True),
|
|
@@ -1271,11 +1312,11 @@ def config_transform(
|
|
|
1271
1312
|
)
|
|
1272
1313
|
)
|
|
1273
1314
|
|
|
1274
|
-
if not
|
|
1315
|
+
if not config_diff:
|
|
1275
1316
|
print("No changes detected")
|
|
1276
1317
|
return
|
|
1277
1318
|
|
|
1278
|
-
print("".join(
|
|
1319
|
+
print("".join(config_diff))
|
|
1279
1320
|
|
|
1280
1321
|
if not apply:
|
|
1281
1322
|
print()
|
ots_containers/config.py
CHANGED
|
@@ -8,13 +8,17 @@ from pathlib import Path
|
|
|
8
8
|
DEFAULT_IMAGE = "ghcr.io/onetimesecret/onetimesecret"
|
|
9
9
|
DEFAULT_TAG = "current"
|
|
10
10
|
|
|
11
|
+
# Config files that ship as defaults in the container image (etc/defaults/*.defaults.yaml).
|
|
12
|
+
# Only files present on the host override the container's built-in defaults.
|
|
13
|
+
CONFIG_FILES: tuple[str, ...] = ("config.yaml", "auth.yaml", "logging.yaml")
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
@dataclass
|
|
13
17
|
class Config:
|
|
14
18
|
"""FHS-compliant configuration paths.
|
|
15
19
|
|
|
16
20
|
Directory layout:
|
|
17
|
-
/etc/onetimesecret/ - YAML
|
|
21
|
+
/etc/onetimesecret/ - YAML config overrides (per-file mount, optional)
|
|
18
22
|
/etc/default/onetimesecret - Infrastructure env vars (REDIS_URL, etc.)
|
|
19
23
|
/var/lib/onetimesecret/ - Variable runtime data (deployments.db)
|
|
20
24
|
/etc/containers/systemd/ - Quadlet unit files
|
|
@@ -125,13 +129,22 @@ class Config:
|
|
|
125
129
|
user_dir = xdg_data / "ots-containers"
|
|
126
130
|
return user_dir / "deployments.db"
|
|
127
131
|
|
|
132
|
+
@property
|
|
133
|
+
def existing_config_files(self) -> list[Path]:
|
|
134
|
+
"""Host config files that exist and should be mounted into the container.
|
|
135
|
+
|
|
136
|
+
Only files present on the host override the container's built-in defaults.
|
|
137
|
+
"""
|
|
138
|
+
return [self.config_dir / f for f in CONFIG_FILES if (self.config_dir / f).exists()]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def has_custom_config(self) -> bool:
|
|
142
|
+
"""Whether any host config files exist to mount."""
|
|
143
|
+
return len(self.existing_config_files) > 0
|
|
144
|
+
|
|
128
145
|
def validate(self) -> None:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
]
|
|
132
|
-
missing = [f for f in required if not f.exists()]
|
|
133
|
-
if missing:
|
|
134
|
-
raise SystemExit(f"Missing required files: {missing}")
|
|
146
|
+
"""Validate configuration. Config files are optional (container has defaults)."""
|
|
147
|
+
pass
|
|
135
148
|
|
|
136
149
|
def resolve_image_tag(self) -> tuple[str, str]:
|
|
137
150
|
"""Resolve image and tag, checking database aliases if tag is an alias.
|
ots_containers/quadlet.py
CHANGED
|
@@ -35,8 +35,9 @@ WEB_TEMPLATE = """\
|
|
|
35
35
|
# # This reads SECRET_VARIABLE_NAMES from the env file,
|
|
36
36
|
# # creates podman secrets, and updates the file
|
|
37
37
|
#
|
|
38
|
-
# 2. Place
|
|
39
|
-
# config.yaml, auth.yaml, logging.yaml
|
|
38
|
+
# 2. (Optional) Place config overrides in {config_dir}/:
|
|
39
|
+
# config.yaml, auth.yaml, logging.yaml
|
|
40
|
+
# Only files present on host are mounted; others use container defaults.
|
|
40
41
|
#
|
|
41
42
|
# OPERATIONS:
|
|
42
43
|
# Start: systemctl start onetime-web@7043
|
|
@@ -67,8 +68,8 @@ RestartSec=5
|
|
|
67
68
|
Image={image}
|
|
68
69
|
Network=host
|
|
69
70
|
|
|
70
|
-
# Syslog tag for
|
|
71
|
-
PodmanArgs=--log-opt tag=onetime
|
|
71
|
+
# Syslog tag for per-instance log filtering: journalctl -t onetime-web-7043 -f
|
|
72
|
+
PodmanArgs=--log-opt tag=onetime-web-%i
|
|
72
73
|
|
|
73
74
|
# Port is derived from instance name: onetime-web@7043 -> PORT=7043
|
|
74
75
|
Environment=PORT=%i
|
|
@@ -79,8 +80,8 @@ EnvironmentFile=/etc/default/onetimesecret
|
|
|
79
80
|
|
|
80
81
|
{secrets_section}
|
|
81
82
|
|
|
82
|
-
#
|
|
83
|
-
|
|
83
|
+
# Host config overrides (per-file, only what exists on host)
|
|
84
|
+
{config_volumes_section}
|
|
84
85
|
|
|
85
86
|
# Static assets extracted from container image
|
|
86
87
|
Volume=static_assets:/app/public:ro
|
|
@@ -121,6 +122,20 @@ def get_secrets_section(env_file_path: Path | None = None) -> str:
|
|
|
121
122
|
return generate_quadlet_secret_lines(secrets)
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
def get_config_volumes_section(cfg: Config) -> str:
|
|
126
|
+
"""Generate per-file Volume directives for host config overrides.
|
|
127
|
+
|
|
128
|
+
Only mounts files that exist on the host. Missing files use container defaults.
|
|
129
|
+
"""
|
|
130
|
+
files = cfg.existing_config_files
|
|
131
|
+
if not files:
|
|
132
|
+
return "# No host config overrides (using container built-in defaults)"
|
|
133
|
+
lines = []
|
|
134
|
+
for f in files:
|
|
135
|
+
lines.append(f"Volume={f}:/app/etc/{f.name}:ro")
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
|
|
124
139
|
def write_web_template(cfg: Config, env_file_path: Path | None = None) -> None:
|
|
125
140
|
"""Write the web container quadlet template.
|
|
126
141
|
|
|
@@ -129,11 +144,13 @@ def write_web_template(cfg: Config, env_file_path: Path | None = None) -> None:
|
|
|
129
144
|
env_file_path: Optional path to environment file for secret discovery
|
|
130
145
|
"""
|
|
131
146
|
secrets_section = get_secrets_section(env_file_path)
|
|
147
|
+
config_volumes_section = get_config_volumes_section(cfg)
|
|
132
148
|
|
|
133
149
|
content = WEB_TEMPLATE.format(
|
|
134
150
|
image=cfg.resolved_image_with_tag,
|
|
135
151
|
config_dir=cfg.config_dir,
|
|
136
152
|
secrets_section=secrets_section,
|
|
153
|
+
config_volumes_section=config_volumes_section,
|
|
137
154
|
)
|
|
138
155
|
cfg.web_template_path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
156
|
cfg.web_template_path.write_text(content)
|
|
@@ -156,8 +173,9 @@ WORKER_TEMPLATE = """\
|
|
|
156
173
|
# # This reads SECRET_VARIABLE_NAMES from the env file,
|
|
157
174
|
# # creates podman secrets, and updates the file
|
|
158
175
|
#
|
|
159
|
-
# 2. Place
|
|
160
|
-
# config.yaml, auth.yaml, logging.yaml
|
|
176
|
+
# 2. (Optional) Place config overrides in {config_dir}/:
|
|
177
|
+
# config.yaml, auth.yaml, logging.yaml
|
|
178
|
+
# Only files present on host are mounted; others use container defaults.
|
|
161
179
|
#
|
|
162
180
|
# OPERATIONS:
|
|
163
181
|
# Start: systemctl start onetime-worker@1
|
|
@@ -194,8 +212,8 @@ TimeoutStopSec=90
|
|
|
194
212
|
Image={image}
|
|
195
213
|
Network=host
|
|
196
214
|
|
|
197
|
-
# Syslog tag for
|
|
198
|
-
PodmanArgs=--log-opt tag=onetime
|
|
215
|
+
# Syslog tag for per-instance log filtering: journalctl -t onetime-worker-1 -f
|
|
216
|
+
PodmanArgs=--log-opt tag=onetime-worker-%i
|
|
199
217
|
|
|
200
218
|
# Worker ID is derived from instance name: onetime-worker@1 -> WORKER_ID=1
|
|
201
219
|
Environment=WORKER_ID=%i
|
|
@@ -206,8 +224,8 @@ EnvironmentFile=/etc/default/onetimesecret
|
|
|
206
224
|
|
|
207
225
|
{secrets_section}
|
|
208
226
|
|
|
209
|
-
#
|
|
210
|
-
|
|
227
|
+
# Host config overrides (per-file, only what exists on host)
|
|
228
|
+
{config_volumes_section}
|
|
211
229
|
|
|
212
230
|
# Worker entry point - runs Sneakers job processor
|
|
213
231
|
Exec=bin/entrypoint.sh bin/ots worker
|
|
@@ -231,11 +249,13 @@ def write_worker_template(cfg: Config, env_file_path: Path | None = None) -> Non
|
|
|
231
249
|
env_file_path: Optional path to environment file for secret discovery
|
|
232
250
|
"""
|
|
233
251
|
secrets_section = get_secrets_section(env_file_path)
|
|
252
|
+
config_volumes_section = get_config_volumes_section(cfg)
|
|
234
253
|
|
|
235
254
|
content = WORKER_TEMPLATE.format(
|
|
236
255
|
image=cfg.resolved_image_with_tag,
|
|
237
256
|
config_dir=cfg.config_dir,
|
|
238
257
|
secrets_section=secrets_section,
|
|
258
|
+
config_volumes_section=config_volumes_section,
|
|
239
259
|
)
|
|
240
260
|
cfg.worker_template_path.parent.mkdir(parents=True, exist_ok=True)
|
|
241
261
|
cfg.worker_template_path.write_text(content)
|
|
@@ -258,8 +278,9 @@ SCHEDULER_TEMPLATE = """\
|
|
|
258
278
|
# # This reads SECRET_VARIABLE_NAMES from the env file,
|
|
259
279
|
# # creates podman secrets, and updates the file
|
|
260
280
|
#
|
|
261
|
-
# 2. Place
|
|
262
|
-
# config.yaml, auth.yaml, logging.yaml
|
|
281
|
+
# 2. (Optional) Place config overrides in {config_dir}/:
|
|
282
|
+
# config.yaml, auth.yaml, logging.yaml
|
|
283
|
+
# Only files present on host are mounted; others use container defaults.
|
|
263
284
|
#
|
|
264
285
|
# OPERATIONS:
|
|
265
286
|
# Start: systemctl start onetime-scheduler@main
|
|
@@ -296,8 +317,8 @@ TimeoutStopSec=60
|
|
|
296
317
|
Image={image}
|
|
297
318
|
Network=host
|
|
298
319
|
|
|
299
|
-
# Syslog tag for
|
|
300
|
-
PodmanArgs=--log-opt tag=onetime
|
|
320
|
+
# Syslog tag for per-instance log filtering: journalctl -t onetime-scheduler-main -f
|
|
321
|
+
PodmanArgs=--log-opt tag=onetime-scheduler-%i
|
|
301
322
|
|
|
302
323
|
# Scheduler ID is derived from instance name: onetime-scheduler@main -> SCHEDULER_ID=main
|
|
303
324
|
Environment=SCHEDULER_ID=%i
|
|
@@ -308,8 +329,8 @@ EnvironmentFile=/etc/default/onetimesecret
|
|
|
308
329
|
|
|
309
330
|
{secrets_section}
|
|
310
331
|
|
|
311
|
-
#
|
|
312
|
-
|
|
332
|
+
# Host config overrides (per-file, only what exists on host)
|
|
333
|
+
{config_volumes_section}
|
|
313
334
|
|
|
314
335
|
# Scheduler entry point - runs scheduled job processor
|
|
315
336
|
Exec=bin/entrypoint.sh bin/ots scheduler
|
|
@@ -333,11 +354,13 @@ def write_scheduler_template(cfg: Config, env_file_path: Path | None = None) ->
|
|
|
333
354
|
env_file_path: Optional path to environment file for secret discovery
|
|
334
355
|
"""
|
|
335
356
|
secrets_section = get_secrets_section(env_file_path)
|
|
357
|
+
config_volumes_section = get_config_volumes_section(cfg)
|
|
336
358
|
|
|
337
359
|
content = SCHEDULER_TEMPLATE.format(
|
|
338
360
|
image=cfg.resolved_image_with_tag,
|
|
339
361
|
config_dir=cfg.config_dir,
|
|
340
362
|
secrets_section=secrets_section,
|
|
363
|
+
config_volumes_section=config_volumes_section,
|
|
341
364
|
)
|
|
342
365
|
cfg.scheduler_template_path.parent.mkdir(parents=True, exist_ok=True)
|
|
343
366
|
cfg.scheduler_template_path.write_text(content)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ots-containers
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Service orchestration for OneTimeSecret: Podman Quadlets and systemd service management
|
|
5
5
|
Project-URL: Homepage, https://onetimesecret.com
|
|
6
|
-
Project-URL: Repository, https://github.com/onetimesecret/
|
|
7
|
-
Project-URL: Issues, https://github.com/onetimesecret/
|
|
6
|
+
Project-URL: Repository, https://github.com/onetimesecret/ots-containers
|
|
7
|
+
Project-URL: Issues, https://github.com/onetimesecret/ots-containers/issues
|
|
8
8
|
Author-email: OneTime Secret <ops@onetimesecret.com>
|
|
9
9
|
License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
ots_containers/__init__.py,sha256=
|
|
1
|
+
ots_containers/__init__.py,sha256=l3Q-SXiufUFERAP1ylkNrtgyPV6mevwrF6LDHAkslPg,204
|
|
2
2
|
ots_containers/assets.py,sha256=BknF2R8z9hpi9IZPUr9PKFKRNsHJxhSAc5PJmUjpemw,1128
|
|
3
3
|
ots_containers/cli.py,sha256=aYXszS81cFxbqzXNy_hpD6VTc_-qTLovAaC2c1zn6jc,2027
|
|
4
|
-
ots_containers/config.py,sha256=
|
|
4
|
+
ots_containers/config.py,sha256=FwBDcCa7apT1gK84ywfCXuzwQVBuETz3I0Sx7jKj9XM,6601
|
|
5
5
|
ots_containers/db.py,sha256=06EZ6W3lWjuFNsmUbbZM_X9kbTCdKAaGChBDCl01bZA,18404
|
|
6
6
|
ots_containers/environment_file.py,sha256=-xtkwl8SM-gsJ2l2A_xm6ImmzD0NIh7_3qcDUjfBjb0,16054
|
|
7
7
|
ots_containers/podman.py,sha256=TQR7WqhwZ5VGb57prKQ5idhQ6jwh9dJEzL-lCA6GwyU,1929
|
|
8
|
-
ots_containers/quadlet.py,sha256=
|
|
8
|
+
ots_containers/quadlet.py,sha256=LDlMqPun6jUxqiOLQ0CYs_8cUM4WKuqqNNqz578bTRE,11636
|
|
9
9
|
ots_containers/systemd.py,sha256=zzTdQSLkmtHcQgmmsCrhKg3vs134XqHvCSjzOYjibFE,9246
|
|
10
10
|
ots_containers/commands/README.md,sha256=eeSXSQYbOQDAivYPhHpDnD40AY5s56mhsBMte5IWTvg,4296
|
|
11
11
|
ots_containers/commands/__init__.py,sha256=aqI3eassjlPRm1hXU-Nk1YzOULNmEYs9EL-REYiHl9M,279
|
|
12
|
-
ots_containers/commands/assets.py,sha256=
|
|
12
|
+
ots_containers/commands/assets.py,sha256=WX8xqk72rln5Nlxzqd0gvBc2U06hZfBZ4HPcnRWnEHM,808
|
|
13
13
|
ots_containers/commands/common.py,sha256=QWOikWfwftxphIOiebHS8usS_f_YlTzW7p23XvUvAxo,1220
|
|
14
|
-
ots_containers/commands/init.py,sha256=
|
|
14
|
+
ots_containers/commands/init.py,sha256=s9QYoPreK2xM3jCM44WgeTx6zAbBG9BcZq0dAtHpz1g,8526
|
|
15
15
|
ots_containers/commands/cloudinit/__init__.py,sha256=cFmkf3KCjAold1kLiIXpTIwETDLn4emr_kWRx-N3EFs,159
|
|
16
16
|
ots_containers/commands/cloudinit/app.py,sha256=X8quXpbz-pdJbuZ9yiFW2V5i1UQPHdfU0wVK1Fs9EvI,4830
|
|
17
17
|
ots_containers/commands/cloudinit/templates.py,sha256=dcCRbbCRpkEFuWnzyLno2mjz4FBO2XNjP-N80g-RAzc,4155
|
|
@@ -20,9 +20,9 @@ ots_containers/commands/env/app.py,sha256=IaOOiTJVn5v7LFEmy2DHloB2zULABXi7WhzQAa
|
|
|
20
20
|
ots_containers/commands/image/__init__.py,sha256=IEAa7vPt82Puu8wp9NGZ4o3p13n-XdmDMjhkPJMWKNE,216
|
|
21
21
|
ots_containers/commands/image/app.py,sha256=ItYOOToqkul4cTNcBXrNTuXvoBfr7NM6EzqrgyfCw6M,32605
|
|
22
22
|
ots_containers/commands/instance/__init__.py,sha256=hwLpdy0UVV1x2-gS8qWgZnZpvYtGY4OB2kyyVYf0xNU,945
|
|
23
|
-
ots_containers/commands/instance/_helpers.py,sha256=
|
|
24
|
-
ots_containers/commands/instance/annotations.py,sha256=
|
|
25
|
-
ots_containers/commands/instance/app.py,sha256=
|
|
23
|
+
ots_containers/commands/instance/_helpers.py,sha256=Ru3RFSEUkoRC_XRDE5y7TGY9qGuiXnYzvxjQNqXtHbI,6225
|
|
24
|
+
ots_containers/commands/instance/annotations.py,sha256=wm6ESdREoSSgzjmD4z_i7KSbx9Gn673O0bs8YaI8zdM,2723
|
|
25
|
+
ots_containers/commands/instance/app.py,sha256=tPDzM1xFpcr5S5yGsTrtoBP0igFb_Dv1Ls5abKluChM,45171
|
|
26
26
|
ots_containers/commands/proxy/__init__.py,sha256=jW4YWoOWr5Ed_RHmVRDekHLcVMUpGfvMPD1kPGti_Ms,193
|
|
27
27
|
ots_containers/commands/proxy/_helpers.py,sha256=KG-4CH9uc96FQt80uppPYH3h_3eVJtxrUZ0l_1HvgHQ,2759
|
|
28
28
|
ots_containers/commands/proxy/app.py,sha256=qVBEdNut5dMjrB7U-NopnrUFbWtpiIYf4Sdmp92qfSQ,4221
|
|
@@ -30,8 +30,8 @@ ots_containers/commands/service/__init__.py,sha256=EraBKTJaG_r6wPbmxhGU4adBdIr6M
|
|
|
30
30
|
ots_containers/commands/service/_helpers.py,sha256=DX8vpnvVgNvE2qv8UCD9ZMbK5i57STNz9oF9Jx68kqw,8568
|
|
31
31
|
ots_containers/commands/service/app.py,sha256=jXRqbjF5t9FvMHCPS6Uid1_N-DJd5WQ_B6vnqT7LmDw,15401
|
|
32
32
|
ots_containers/commands/service/packages.py,sha256=Diqt7lMMaaRpv9isM67xqq9fX3sNP_Oam04kYYlVibI,7008
|
|
33
|
-
ots_containers-0.3.
|
|
34
|
-
ots_containers-0.3.
|
|
35
|
-
ots_containers-0.3.
|
|
36
|
-
ots_containers-0.3.
|
|
37
|
-
ots_containers-0.3.
|
|
33
|
+
ots_containers-0.3.2.dist-info/METADATA,sha256=QfFlDCRhhxiEb6s7aeHsfodDW8R-UemUD4SF0UAtKwI,8012
|
|
34
|
+
ots_containers-0.3.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
35
|
+
ots_containers-0.3.2.dist-info/entry_points.txt,sha256=qL4eUBGdNuv-nOybKMxkKCJABXKzUXdlAE8d54G13Og,58
|
|
36
|
+
ots_containers-0.3.2.dist-info/licenses/LICENSE,sha256=Wck3mAODGbVKhVuVxnuyTj7_1jQTSyzZGoOxeSQO3KQ,1066
|
|
37
|
+
ots_containers-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|