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.
@@ -1,3 +1,5 @@
1
1
  # src/ots_containers/__init__.py
2
2
 
3
- __version__ = "0.2.2"
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("ots-containers")
@@ -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
@@ -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.
@@ -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 = [cfg.web_template_path, cfg.worker_template_path, cfg.scheduler_template_path]
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 format_journalctl_hint(instances: dict[InstanceType, list[str]]) -> str:
23
- """Generate a journalctl command to view logs for the given instances.
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
- instances: Dict mapping InstanceType to list of identifiers
32
+ env_file: Path to environment file (e.g., /etc/default/onetimesecret)
27
33
 
28
34
  Returns:
29
- A journalctl command string with -t flags for each instance.
35
+ List of command arguments: ["--secret", "name,type=env,target=VAR", ...]
30
36
  """
31
- tags = []
32
- for itype, ids in instances.items():
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
- tag_args = " ".join(f"-t {tag}" for tag in tags)
40
- return f"journalctl {tag_args} -f"
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
@@ -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: