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.
@@ -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
- __version__ = version("ots-containers")
5
+ try:
6
+ __version__ = version("ots-containers")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0+dev"
@@ -26,5 +26,4 @@ def sync(
26
26
  on initial setup.
27
27
  """
28
28
  cfg = Config()
29
- cfg.validate()
30
29
  assets_module.update(cfg, create_volume=create_volume)
@@ -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
- if cfg.config_yaml.exists():
135
- print(f" [ok] {cfg.config_yaml}")
136
- else:
137
- print(f" [missing] {cfg.config_yaml}")
138
- all_ok = False
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
- if _copy_template(src / "config.yaml", cfg.config_yaml, quiet=quiet) is None:
148
- all_ok = False
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
- # Report status of config files
151
- if cfg.config_yaml.exists():
152
- print(f" [ok] {cfg.config_yaml}")
153
- else:
154
- print(f" [missing] {cfg.config_yaml}")
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
- if not cfg.config_yaml.exists():
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 Enum
16
+ from enum import StrEnum
17
17
  from typing import Annotated
18
18
 
19
19
  import cyclopts
20
20
 
21
21
 
22
- class InstanceType(str, Enum):
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
- # Volumes
328
- cmd.extend(["-v", f"{cfg.config_dir}:/app/etc:ro"])
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
- print(f"Reading config from {cfg.config_yaml}")
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=str(e),
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
- print(f"Reading config from {cfg.config_yaml}")
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=str(e),
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=str(e),
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 directory (always read-only)
1063
- # Resolve symlinks for podman VM compatibility (macOS)
1064
- config_dir = cfg.config_dir.resolve()
1065
- cmd.extend(["-v", f"{config_dir}:/app/etc:ro"])
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
- # Generate and display diff
1265
- diff = list(
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 diff:
1315
+ if not config_diff:
1275
1316
  print("No changes detected")
1276
1317
  return
1277
1318
 
1278
- print("".join(diff))
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 configs mounted as /app/etc:ro
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
- required = [
130
- self.config_yaml,
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 YAML configs in {config_dir}/:
39
- # config.yaml, auth.yaml, logging.yaml, billing.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 unified log filtering: journalctl -t onetime -f
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
- # Config directory mounted read-only (all YAML configs)
83
- Volume={config_dir}:/app/etc:ro
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 YAML configs in {config_dir}/:
160
- # config.yaml, auth.yaml, logging.yaml, billing.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 unified log filtering: journalctl -t onetime -f
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
- # Config directory mounted read-only (all YAML configs)
210
- Volume={config_dir}:/app/etc:ro
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 YAML configs in {config_dir}/:
262
- # config.yaml, auth.yaml, logging.yaml, billing.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 unified log filtering: journalctl -t onetime -f
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
- # Config directory mounted read-only (all YAML configs)
312
- Volume={config_dir}:/app/etc:ro
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.1
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/ots_containers
7
- Project-URL: Issues, https://github.com/onetimesecret/ots_containers/issues
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=6onCUg0P0pB4QGQ3tESnfFftdxr1fR1HSdEeFc_WEas,114
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=7cUMLMDozS2lODPIezNkSjOG14oFBvGj48pfBanCCQw,5940
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=hYoR2LfFPTHPnGY-bq_D78mQnSsaibNsKg5w-X3OlkU,10512
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=PJtUme0ko7kfMXzKRU84_o42WPGmwt_CM6sgwEfDGz0,827
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=7VH-czZz1lHB3FEzarkIMpJ9TB59UpNB1AOgIY0AXwc,8391
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=rPVGYbBjvN5DaHvHJvm3tDdV6tyEG1AWJQOuaWAKqYU,5379
24
- ots_containers/commands/instance/annotations.py,sha256=cGzBsLEELcYborJK9ompbVrc5R7pmEEX46MlL6Mo-cU,2722
25
- ots_containers/commands/instance/app.py,sha256=a7DVVaDovJh4T7ty03zL3wEGBbjggQ2GMXTNjAIJO88,43289
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.1.dist-info/METADATA,sha256=Y6fa8hbDfLgaBodUL0hfnD7ZxPtkHgbvxWFpoOoSY9k,8012
34
- ots_containers-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
35
- ots_containers-0.3.1.dist-info/entry_points.txt,sha256=qL4eUBGdNuv-nOybKMxkKCJABXKzUXdlAE8d54G13Og,58
36
- ots_containers-0.3.1.dist-info/licenses/LICENSE,sha256=Wck3mAODGbVKhVuVxnuyTj7_1jQTSyzZGoOxeSQO3KQ,1066
37
- ots_containers-0.3.1.dist-info/RECORD,,
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,,