compose2pod 0.3.0__tar.gz → 0.3.1__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compose2pod
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Convert a Docker Compose file into a script that runs its services as a single Podman pod
5
5
  Keywords: podman,docker-compose,compose,pod,ci,testing,containers
6
6
  Author: Artur Shiriev
@@ -83,15 +83,35 @@ sh ./run.sh
83
83
 
84
84
  ## Supported compose subset
85
85
 
86
- compose2pod supports an honest subset and errors clearly on anything outside
87
- it: `image`/`build`, `command`, `environment`/`env_file`, short-form bind
88
- `volumes`, `healthcheck` (CMD/CMD-SHELL), `depends_on` (all conditions), and
89
- network `aliases`. Compose extension fields (any `x-`-prefixed key) and YAML
90
- anchors are accepted as-is, so a top-level `x-*` anchor block for shared
91
- config just works. `${VAR}`-style variable interpolation is left live in the
92
- generated script, resolved by its shell against the environment present
93
- when the script runs (no `.env` file support). See
94
- `architecture/supported-subset.md` for the full accept/ignore/reject matrix.
86
+ compose2pod refuses **every document `docker compose config` refuses** — a
87
+ measured property, checked continuously by a differential conformance harness
88
+ that runs the real Docker CLI and the real compose2pod pipeline over the same
89
+ YAML. So a file that compiles is a file Docker would run; where compose2pod
90
+ still refuses a form Docker accepts, it is because Podman genuinely cannot
91
+ express it (each such case is documented, not guessed).
92
+
93
+ Within that boundary it covers most of what real compose files use:
94
+
95
+ - **Services** — `image`/`build`, `command`/`entrypoint`, `environment` and
96
+ `env_file` (string and long-form `{path, required, format}`), `volumes`
97
+ (short-form and long-form `--mount`, including the `bind`/`volume`/`tmpfs`
98
+ option maps), `tmpfs`, `healthcheck`, `depends_on` (all conditions), network
99
+ `aliases`, `hostname`/`container_name`.
100
+ - **Confinement & metadata** — `user`, `working_dir`, `read_only`, `init`,
101
+ `privileged`, `cap_add`/`cap_drop`, `security_opt`, `devices`, `group_add`,
102
+ `platform`, `labels`, `annotations`, `pull_policy` (the quoted-boolean and
103
+ YAML-1.1 spellings Docker accepts, too).
104
+ - **Resources** — the legacy keys (`mem_limit`, `cpus`, `pids_limit`,
105
+ `ulimits`, …) and the modern `deploy.resources` block.
106
+ - **Pod-wide** — `dns`/`dns_search`/`dns_opt`, `sysctls`, `extra_hosts`.
107
+ - **Composition** — same-file `extends`, `secrets`/`configs`, `profiles`.
108
+
109
+ Compose extension fields (any `x-`-prefixed key) and YAML anchors are accepted
110
+ as-is, so a top-level `x-*` anchor block for shared config just works.
111
+ `${VAR}`-style variable interpolation is left live in the generated script,
112
+ resolved by its shell against the environment present when the script runs (no
113
+ `.env` file support). See `architecture/supported-subset.md` for the full
114
+ accept/ignore/reject matrix and `planning/decisions/` for the boundary rulings.
95
115
 
96
116
  ## Status
97
117
 
@@ -57,15 +57,35 @@ sh ./run.sh
57
57
 
58
58
  ## Supported compose subset
59
59
 
60
- compose2pod supports an honest subset and errors clearly on anything outside
61
- it: `image`/`build`, `command`, `environment`/`env_file`, short-form bind
62
- `volumes`, `healthcheck` (CMD/CMD-SHELL), `depends_on` (all conditions), and
63
- network `aliases`. Compose extension fields (any `x-`-prefixed key) and YAML
64
- anchors are accepted as-is, so a top-level `x-*` anchor block for shared
65
- config just works. `${VAR}`-style variable interpolation is left live in the
66
- generated script, resolved by its shell against the environment present
67
- when the script runs (no `.env` file support). See
68
- `architecture/supported-subset.md` for the full accept/ignore/reject matrix.
60
+ compose2pod refuses **every document `docker compose config` refuses** — a
61
+ measured property, checked continuously by a differential conformance harness
62
+ that runs the real Docker CLI and the real compose2pod pipeline over the same
63
+ YAML. So a file that compiles is a file Docker would run; where compose2pod
64
+ still refuses a form Docker accepts, it is because Podman genuinely cannot
65
+ express it (each such case is documented, not guessed).
66
+
67
+ Within that boundary it covers most of what real compose files use:
68
+
69
+ - **Services** — `image`/`build`, `command`/`entrypoint`, `environment` and
70
+ `env_file` (string and long-form `{path, required, format}`), `volumes`
71
+ (short-form and long-form `--mount`, including the `bind`/`volume`/`tmpfs`
72
+ option maps), `tmpfs`, `healthcheck`, `depends_on` (all conditions), network
73
+ `aliases`, `hostname`/`container_name`.
74
+ - **Confinement & metadata** — `user`, `working_dir`, `read_only`, `init`,
75
+ `privileged`, `cap_add`/`cap_drop`, `security_opt`, `devices`, `group_add`,
76
+ `platform`, `labels`, `annotations`, `pull_policy` (the quoted-boolean and
77
+ YAML-1.1 spellings Docker accepts, too).
78
+ - **Resources** — the legacy keys (`mem_limit`, `cpus`, `pids_limit`,
79
+ `ulimits`, …) and the modern `deploy.resources` block.
80
+ - **Pod-wide** — `dns`/`dns_search`/`dns_opt`, `sysctls`, `extra_hosts`.
81
+ - **Composition** — same-file `extends`, `secrets`/`configs`, `profiles`.
82
+
83
+ Compose extension fields (any `x-`-prefixed key) and YAML anchors are accepted
84
+ as-is, so a top-level `x-*` anchor block for shared config just works.
85
+ `${VAR}`-style variable interpolation is left live in the generated script,
86
+ resolved by its shell against the environment present when the script runs (no
87
+ `.env` file support). See `architecture/supported-subset.md` for the full
88
+ accept/ignore/reject matrix and `planning/decisions/` for the boundary rulings.
69
89
 
70
90
  ## Status
71
91
 
@@ -95,15 +95,15 @@ def _env_file_flags(svc: dict[str, Any], project_dir: str) -> list[Token]:
95
95
 
96
96
 
97
97
  def _nested_mount_parts(vtype: str, options: dict[str, Any]) -> list[str]:
98
- """Map a long-form volume entry's validated `bind`/`volume`/`tmpfs` sub-map to `--mount` options."""
98
+ """Map a long-form volume entry's validated `bind`/`volume`/`tmpfs`/`image` sub-map to `--mount` options."""
99
99
  if vtype == "bind":
100
100
  parts = [f"bind-propagation={options['propagation']}"] if "propagation" in options else []
101
101
  if "selinux" in options:
102
102
  parts.append(f"relabel={'shared' if options['selinux'] == 'z' else 'private'}")
103
103
  return parts
104
- if vtype == "volume":
104
+ if vtype in ("volume", "image"):
105
105
  return [f"subpath={options['subpath']}"] if "subpath" in options else []
106
- # vtype == "tmpfs": the gate validates `type` to one of bind/volume/tmpfs, so no other branch is reachable.
106
+ # vtype == "tmpfs": the gate validates `type` to bind/volume/tmpfs/image, so no other branch is reachable.
107
107
  parts = [f"tmpfs-size={options['size']}"] if "size" in options else []
108
108
  if "mode" in options:
109
109
  parts.append(f"tmpfs-mode={options['mode']}")
@@ -119,7 +119,7 @@ def _mount_flag(entry: dict[str, Any], project_dir: str) -> list[Token]:
119
119
  source = str(Path(project_dir, source))
120
120
  parts.append(f"source={source}")
121
121
  parts.append(f"target={entry['target']}")
122
- if as_bool(entry.get("read_only", False)):
122
+ if entry["type"] != "image" and as_bool(entry.get("read_only", False)):
123
123
  parts.append("ro")
124
124
  vtype = entry["type"]
125
125
  parts += _nested_mount_parts(vtype, entry.get(vtype, {}))
@@ -160,7 +160,7 @@ def _classify_volume(volume: str) -> tuple[str, str | None]:
160
160
  return "bind", None
161
161
 
162
162
 
163
- _VOLUME_LONG_TYPES = ("bind", "volume", "tmpfs")
163
+ _VOLUME_LONG_TYPES = ("bind", "volume", "tmpfs", "image")
164
164
  _VOLUME_LONG_KEYS = {"type", "source", "target", "read_only", "consistency"}
165
165
  # Docker's own per-type nested option map keys (measured, docker compose config
166
166
  # v5.1.2). `create_host_path`/`nocopy` are real docker keys, so they land here
@@ -171,6 +171,7 @@ _VOLUME_OPTION_KEYS = {
171
171
  "bind": {"propagation", "selinux", "create_host_path"},
172
172
  "volume": {"subpath", "nocopy"},
173
173
  "tmpfs": {"size", "mode"},
174
+ "image": {"subpath"},
174
175
  }
175
176
  _PROPAGATION_VALUES = {"private", "rprivate", "shared", "rshared", "slave", "rslave"}
176
177
  _SELINUX_VALUES = {"z", "Z"}
@@ -205,10 +206,11 @@ def _validate_service_volumes(name: str, svc: dict[str, Any]) -> None:
205
206
  def _validate_volume_long_form(name: str, entry: dict[str, Any]) -> None:
206
207
  """Check one long-syntax volume mapping against Docker's strict schema (measured, v5.1.2).
207
208
 
208
- type (bind/volume/tmpfs), source, target, read_only, consistency, plus the
209
- one nested option map matching `type` (a mismatched sub-map is refused --
210
- a deliberate stricter-than-docker check; docker accepts-and-ignores it).
211
- cluster/npipe/image types are refused (podman cannot express them).
209
+ type (bind/volume/tmpfs/image), source, target, read_only, consistency,
210
+ plus the one nested option map matching `type` (a mismatched sub-map is
211
+ refused -- a deliberate stricter-than-docker check; docker
212
+ accepts-and-ignores it). cluster/npipe types are refused (podman cannot
213
+ express them).
212
214
 
213
215
  `type` is validated before the unknown-key check (unlike every other field
214
216
  here) because the check itself needs `vtype` to know which sub-map key --
@@ -303,10 +305,26 @@ def _validate_tmpfs_options(name: str, options: dict[str, Any]) -> None:
303
305
  raise UnsupportedComposeError(msg)
304
306
 
305
307
 
308
+ def _validate_image_options(name: str, options: dict[str, Any]) -> None:
309
+ """Check an image entry's nested `image:` option map (measured: strict, `subpath` only, absolute)."""
310
+ if "subpath" not in options:
311
+ return
312
+ subpath = options["subpath"]
313
+ if not isinstance(subpath, str):
314
+ msg = f"service {name!r}: image 'subpath' must be a string"
315
+ raise UnsupportedComposeError(msg)
316
+ if not subpath.startswith("/") and not values.has_variable(subpath):
317
+ # podman requires an absolute image subpath (`must be an absolute path`);
318
+ # docker accepts a relative one -> a rule-two narrowing. A ${VAR} is host-dependent.
319
+ msg = f"service {name!r}: image 'subpath' must be an absolute path"
320
+ raise UnsupportedComposeError(msg)
321
+
322
+
306
323
  _VOLUME_OPTION_VALIDATORS: dict[str, Callable[[str, dict[str, Any]], None]] = {
307
324
  "bind": _validate_bind_options,
308
325
  "volume": _validate_volume_type_options,
309
326
  "tmpfs": _validate_tmpfs_options,
327
+ "image": _validate_image_options,
310
328
  }
311
329
 
312
330
 
@@ -324,10 +342,10 @@ def _validate_volume_options(name: str, vtype: str, options: Any) -> None: # no
324
342
 
325
343
 
326
344
  def _validate_volume_long_form_source(name: str, vtype: str, source: Any) -> None: # noqa: ANN401 - Compose values are untyped YAML/JSON
327
- """Check a long-form volume entry's 'source': required for bind, refused for tmpfs, optional string for volume."""
328
- if vtype == "bind":
345
+ """Check a long-form volume entry's 'source': required for bind/image, refused for tmpfs, optional for volume."""
346
+ if vtype in ("bind", "image"):
329
347
  if not isinstance(source, str):
330
- msg = f"service {name!r}: bind volume 'source' must be a string"
348
+ msg = f"service {name!r}: {vtype} volume 'source' must be a string"
331
349
  raise UnsupportedComposeError(msg)
332
350
  elif vtype == "tmpfs":
333
351
  if source is not None:
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Topic :: Software Development :: Libraries",
19
19
  "Topic :: Software Development :: Testing",
20
20
  ]
21
- version = "0.3.0"
21
+ version = "0.3.1"
22
22
 
23
23
  [project.optional-dependencies]
24
24
  yaml = ["PyYAML>=6"]