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.
@@ -1,3 +1,8 @@
1
1
  # src/ots_containers/__init__.py
2
2
 
3
- __version__ = "0.2.2"
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("ots-containers")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0+dev"
@@ -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 | Systemd Unit | Identifier | Use |
16
- |------|--------------|------------|-----|
17
- | `web` | `onetime-web@{port}` | Port number | HTTP servers |
18
- | `worker` | `onetime-worker@{id}` | Name or number | Background jobs |
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 | 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 |
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 | Example | Use for |
72
- |---------|---------|---------|
73
- | Verb | `deploy`, `sync` | Actions |
74
- | `--flag` | `--force`, `--yes` | Boolean options |
75
- | `--option VALUE` | `--delay 5`, `--lines 50` | Value options |
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("PostgreSQL repository will use inline key placeholder", file=sys.stderr)
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/cloudinit/templates.py
2
+
2
3
  """Cloud-init configuration templates with Debian 13 DEB822 apt sources."""
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  # src/ots_containers/commands/common.py
2
+
2
3
  """Shared CLI annotations for consistency across commands.
3
4
 
4
5
  All common flags use long+short forms for consistency:
@@ -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("Error: No SECRET_VARIABLE_NAMES defined in environment file.", file=sys.stderr)
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 (e.g., v0.23.0, latest)",
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}:{tag}"
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
- podman.pull(
90
- full_image,
91
- authfile=str(cfg.registry_auth_file),
92
- check=True,
93
- capture_output=True,
94
- text=True,
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=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=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, tag)
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 {tag} (previous: {previous})")
143
+ print(f"Set CURRENT to {resolved_tag} (previous: {previous})")
125
144
  else:
126
- print(f"Set CURRENT to {tag}")
145
+ print(f"Set CURRENT to {resolved_tag}")
127
146
 
128
147
 
129
148
  @app.default
@@ -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
- if cfg.config_yaml.exists():
134
- print(f" [ok] {cfg.config_yaml}")
135
- else:
136
- print(f" [missing] {cfg.config_yaml}")
137
- 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}")
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
- if _copy_template(src / "config.yaml", cfg.config_yaml, quiet=quiet) is None:
147
- 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
148
151
  elif not quiet:
149
- # Report status of config files
150
- if cfg.config_yaml.exists():
151
- print(f" [ok] {cfg.config_yaml}")
152
- else:
153
- 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}")
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 = [cfg.web_template_path, cfg.worker_template_path, cfg.scheduler_template_path]
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
- if not cfg.config_yaml.exists():
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 Enum
16
+ from enum import StrEnum
16
17
  from typing import Annotated
17
18
 
18
19
  import cyclopts
19
20
 
20
21
 
21
- class InstanceType(str, Enum):
22
+ class InstanceType(StrEnum):
22
23
  """Type of container instance."""
23
24
 
24
25
  WEB = "web"