compose2pod 0.4.0__tar.gz → 0.4.2__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.4.0
3
+ Version: 0.4.2
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
@@ -335,7 +335,13 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript:
335
335
  # Only the closure joins the pod, so only the closure is resolvable. A name
336
336
  # pointing at 127.0.0.1 for a service that never runs would turn an honest
337
337
  # resolution failure into a connection-refused.
338
- hosts = hostnames({name: services[name] for name in order})
338
+ # The pod name is added as a resolvable name so every container's own
339
+ # hostname (set to the pod name below) resolves. A pod shares one UTS
340
+ # namespace, so without this the hostname is the random container ID,
341
+ # absent from the owned /etc/hosts -- `hostname -f` then fails and images
342
+ # that resolve their own hostname at startup crash. Podman used to add this
343
+ # line itself; `--no-hosts` stops it, so the script owns it too.
344
+ hosts = [*hostnames({name: services[name] for name in order}), options.pod]
339
345
  host_tokens = hosts_file_tokens(services, order, hosts)
340
346
  completion_gated = {
341
347
  dep
@@ -355,12 +361,20 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript:
355
361
  lines.append(f"trap '{teardown}' EXIT")
356
362
  pod_flags = pod_create_flags(services, order)
357
363
  _collect_vars(pod_flags, names)
358
- pod_create = f"podman pod create --name {shlex.quote(options.pod)} --no-hosts"
364
+ # `--uts private` gives the pod its own UTS namespace so `--hostname` is
365
+ # settable (in the host UTS namespace Podman refuses it); the pod name is
366
+ # the shared hostname, resolvable via the line added to the hosts file above.
367
+ pod_name = shlex.quote(options.pod)
368
+ pod_create = f"podman pod create --name {pod_name} --no-hosts --uts private --hostname {pod_name}"
359
369
  if pod_flags:
360
370
  pod_create += " " + _render(pod_flags)
361
371
  lines.append(pod_create)
362
372
  _collect_vars(host_tokens, names)
363
373
  lines.append(f"printf '%s\\n' {_render(host_tokens)} > \"$hostsfile\"")
374
+ # mktemp creates the file 0600; make it world-readable so a service image
375
+ # running as a non-root user can read the bind-mounted /etc/hosts. Without
376
+ # this, glibc falls through to DNS and name resolution fails.
377
+ lines.append('chmod 644 "$hostsfile"')
364
378
  lines.extend(stores.create_lines(compose, order, options.pod, options.project_dir))
365
379
  names |= stores.referenced_variables(compose, order, options.project_dir)
366
380
  waited: set[str] = set()
@@ -111,8 +111,8 @@ def is_number(value: Any) -> bool: # noqa: ANN401 - Compose values are untyped
111
111
  return not isinstance(value, bool) and isinstance(value, int | float | str)
112
112
 
113
113
 
114
- def require_string_keys(where: str, mapping: dict[Any, Any]) -> None:
115
- """Check every key of a raw YAML/JSON mapping is a string.
114
+ def require_string_keys(where: str, mapping: dict[Any, Any]) -> set[str]:
115
+ """Check every key of a raw YAML/JSON mapping is a string; return them.
116
116
 
117
117
  PyYAML routinely produces non-string keys (a bare `3:` is an int; under
118
118
  YAML 1.1, a bare `on:`/`off:` is a bool). Every mapping-key consumer
@@ -120,11 +120,18 @@ def require_string_keys(where: str, mapping: dict[Any, Any]) -> None:
120
120
  `str` and crashes raw otherwise. This is the one shared check the gate
121
121
  runs before it reads any of `mapping`'s keys, so a non-string key fails
122
122
  clean regardless of which mapping it turned up in.
123
+
124
+ Returns the validated keys as a `set[str]` so an unsupported-key check can
125
+ build on it directly (`require_string_keys(...) - KNOWN`) with a statically
126
+ sortable type, instead of re-deriving `set(mapping)` as `set[object]`.
123
127
  """
128
+ keys: set[str] = set()
124
129
  for key in mapping:
125
130
  if not isinstance(key, str):
126
131
  msg = f"{where}: key {key!r} must be a string"
127
132
  raise UnsupportedComposeError(msg)
133
+ keys.add(key)
134
+ return keys
128
135
 
129
136
 
130
137
  def _validate_list_elements(name: str, key: str, value: list[Any]) -> None:
@@ -216,12 +216,12 @@ def _validate_volume_long_form(name: str, entry: dict[str, Any]) -> None:
216
216
  here) because the check itself needs `vtype` to know which sub-map key --
217
217
  `bind`/`volume`/`tmpfs` -- the entry is allowed to carry alongside it.
218
218
  """
219
- require_string_keys(f"service {name!r}: volume", entry)
219
+ keys = require_string_keys(f"service {name!r}: volume", entry)
220
220
  vtype = entry.get("type")
221
221
  if vtype not in _VOLUME_LONG_TYPES:
222
222
  msg = f"service {name!r}: volume 'type' must be one of {list(_VOLUME_LONG_TYPES)}"
223
223
  raise UnsupportedComposeError(msg)
224
- unknown = set(entry) - _VOLUME_LONG_KEYS - {vtype}
224
+ unknown = keys - _VOLUME_LONG_KEYS - {vtype}
225
225
  if unknown:
226
226
  msg = f"service {name!r}: volume: unsupported keys {sorted(unknown)}"
227
227
  raise UnsupportedComposeError(msg)
@@ -333,8 +333,8 @@ def _validate_volume_options(name: str, vtype: str, options: Any) -> None: # no
333
333
  if not isinstance(options, dict):
334
334
  msg = f"service {name!r}: {vtype} options must be a mapping"
335
335
  raise UnsupportedComposeError(msg)
336
- require_string_keys(f"service {name!r}: {vtype} options", options)
337
- unknown = set(options) - _VOLUME_OPTION_KEYS[vtype]
336
+ keys = require_string_keys(f"service {name!r}: {vtype} options", options)
337
+ unknown = keys - _VOLUME_OPTION_KEYS[vtype]
338
338
  if unknown:
339
339
  msg = f"service {name!r}: {vtype} options: unsupported keys {sorted(unknown)}"
340
340
  raise UnsupportedComposeError(msg)
@@ -529,8 +529,8 @@ def _validate_build_secret_entry(name: str, entry: Any) -> None: # noqa: ANN401
529
529
  if not isinstance(entry, dict):
530
530
  msg = f"service {name!r}: build 'secrets' entries must be a string or mapping"
531
531
  raise UnsupportedComposeError(msg)
532
- require_string_keys(f"service {name!r}: build 'secrets' entry", entry)
533
- unknown = set(entry) - _BUILD_SECRET_REF_KEYS
532
+ keys = require_string_keys(f"service {name!r}: build 'secrets' entry", entry)
533
+ unknown = keys - _BUILD_SECRET_REF_KEYS
534
534
  if unknown:
535
535
  msg = f"service {name!r}: build 'secrets' entry: unsupported keys {sorted(unknown)}"
536
536
  raise UnsupportedComposeError(msg)
@@ -683,8 +683,8 @@ def _validate_env_file(name: str, svc: dict[str, Any]) -> None:
683
683
 
684
684
  def _validate_env_file_mapping(name: str, entry: dict[str, Any]) -> None:
685
685
  """Check one long-form env_file mapping against Docker's strict schema (measured, v5.1.2)."""
686
- require_string_keys(f"service {name!r}: 'env_file'", entry)
687
- unknown = set(entry) - _ENV_FILE_ENTRY_KEYS
686
+ keys = require_string_keys(f"service {name!r}: 'env_file'", entry)
687
+ unknown = keys - _ENV_FILE_ENTRY_KEYS
688
688
  if unknown:
689
689
  msg = f"service {name!r}: 'env_file' entry: unsupported keys {sorted(unknown)}"
690
690
  raise UnsupportedComposeError(msg)
@@ -1240,8 +1240,8 @@ def _validate_definition_external(label: str) -> Callable[[str, str, Any], None]
1240
1240
  if values.is_bool_like(value):
1241
1241
  return
1242
1242
  if isinstance(value, dict):
1243
- require_string_keys(f"{label} {ident!r}: {key!r}", value)
1244
- unknown = set(value) - _EXTERNAL_MAP_KEYS
1243
+ keys = require_string_keys(f"{label} {ident!r}: {key!r}", value)
1244
+ unknown = keys - _EXTERNAL_MAP_KEYS
1245
1245
  if unknown:
1246
1246
  msg = f"{label} {ident!r}: {key!r}: unsupported keys {sorted(unknown)}"
1247
1247
  raise UnsupportedComposeError(msg)
@@ -1278,8 +1278,8 @@ def _validate_ipam_config_entry(label: str, ident: str, entry: Any) -> None: #
1278
1278
  if not isinstance(entry, dict):
1279
1279
  msg = f"{label} {ident!r}: ipam 'config' entries must be a mapping"
1280
1280
  raise UnsupportedComposeError(msg)
1281
- require_string_keys(f"{label} {ident!r}: ipam config entry", entry)
1282
- unknown = set(entry) - _IPAM_CONFIG_ENTRY_KEYS
1281
+ keys = require_string_keys(f"{label} {ident!r}: ipam config entry", entry)
1282
+ unknown = keys - _IPAM_CONFIG_ENTRY_KEYS
1283
1283
  if unknown:
1284
1284
  msg = f"{label} {ident!r}: ipam config entry: unsupported keys {sorted(unknown)}"
1285
1285
  raise UnsupportedComposeError(msg)
@@ -1309,8 +1309,8 @@ def _validate_definition_ipam(label: str) -> Callable[[str, str, Any], None]:
1309
1309
  if not isinstance(value, dict):
1310
1310
  msg = f"{label} {ident!r}: {key!r} must be a mapping"
1311
1311
  raise UnsupportedComposeError(msg)
1312
- require_string_keys(f"{label} {ident!r}: {key!r}", value)
1313
- unknown = set(value) - _IPAM_KEYS
1312
+ keys = require_string_keys(f"{label} {ident!r}: {key!r}", value)
1313
+ unknown = keys - _IPAM_KEYS
1314
1314
  if unknown:
1315
1315
  msg = f"{label} {ident!r}: {key!r}: unsupported keys {sorted(unknown)}"
1316
1316
  raise UnsupportedComposeError(msg)
@@ -36,8 +36,8 @@ def _validate_limits(name: str, svc: dict[str, Any], limits: Any) -> None: # no
36
36
  if not isinstance(limits, dict):
37
37
  msg = f"service {name!r}: deploy.resources.limits must be a mapping"
38
38
  raise UnsupportedComposeError(msg)
39
- require_string_keys(f"service {name!r}: deploy.resources.limits", limits)
40
- unknown = set(limits) - set(_LIMITS)
39
+ keys = require_string_keys(f"service {name!r}: deploy.resources.limits", limits)
40
+ unknown = keys - set(_LIMITS)
41
41
  if unknown:
42
42
  msg = f"service {name!r}: deploy.resources.limits: unsupported keys {sorted(unknown)}"
43
43
  raise UnsupportedComposeError(msg)
@@ -54,8 +54,8 @@ def _validate_reservations(name: str, svc: dict[str, Any], reservations: Any) ->
54
54
  if not isinstance(reservations, dict):
55
55
  msg = f"service {name!r}: deploy.resources.reservations must be a mapping"
56
56
  raise UnsupportedComposeError(msg)
57
- require_string_keys(f"service {name!r}: deploy.resources.reservations", reservations)
58
- unknown = set(reservations) - {"cpus", "memory", "devices"}
57
+ keys = require_string_keys(f"service {name!r}: deploy.resources.reservations", reservations)
58
+ unknown = keys - {"cpus", "memory", "devices"}
59
59
  if unknown:
60
60
  msg = f"service {name!r}: deploy.resources.reservations: unsupported keys {sorted(unknown)}"
61
61
  raise UnsupportedComposeError(msg)
@@ -91,8 +91,8 @@ def validate_deploy(name: str, svc: dict[str, Any]) -> None:
91
91
  if not isinstance(deploy, dict):
92
92
  msg = f"service {name!r}: 'deploy' must be a mapping"
93
93
  raise UnsupportedComposeError(msg)
94
- require_string_keys(f"service {name!r}: deploy", deploy)
95
- unknown = set(deploy) - {"resources"}
94
+ keys = require_string_keys(f"service {name!r}: deploy", deploy)
95
+ unknown = keys - {"resources"}
96
96
  if unknown:
97
97
  msg = f"service {name!r}: deploy: only 'resources' is supported (got {sorted(unknown)})"
98
98
  raise UnsupportedComposeError(msg)
@@ -105,8 +105,8 @@ def validate_deploy(name: str, svc: dict[str, Any]) -> None:
105
105
  raise UnsupportedComposeError(msg)
106
106
  _reject_null_block(name, "deploy.resources.limits", resources, "limits")
107
107
  _reject_null_block(name, "deploy.resources.reservations", resources, "reservations")
108
- require_string_keys(f"service {name!r}: deploy.resources", resources)
109
- unknown = set(resources) - {"limits", "reservations"}
108
+ keys = require_string_keys(f"service {name!r}: deploy.resources", resources)
109
+ unknown = keys - {"limits", "reservations"}
110
110
  if unknown:
111
111
  msg = f"service {name!r}: deploy.resources: unsupported keys {sorted(unknown)}"
112
112
  raise UnsupportedComposeError(msg)
@@ -73,8 +73,8 @@ def _validate_def(name: str, definition: Any, kind: StoreKind) -> None: # noqa:
73
73
  if not isinstance(definition, dict):
74
74
  msg = f"{kind.label} {name!r} must be a mapping"
75
75
  raise UnsupportedComposeError(msg)
76
- require_string_keys(f"{kind.label} {name!r}", definition)
77
- unknown = set(definition) - kind.sources
76
+ keys = require_string_keys(f"{kind.label} {name!r}", definition)
77
+ unknown = keys - kind.sources
78
78
  if unknown:
79
79
  if "external" in unknown:
80
80
  msg = (
@@ -131,8 +131,8 @@ def _ref_source(name: str, ref: Any, kind: StoreKind) -> str: # noqa: ANN401 -
131
131
  if not isinstance(ref, dict):
132
132
  msg = f"service {name!r}: {kind.label} entry must be a string or mapping"
133
133
  raise UnsupportedComposeError(msg)
134
- require_string_keys(f"service {name!r}: {kind.label} entry", ref)
135
- unknown = set(ref) - _LONG_FORM_KEYS
134
+ keys = require_string_keys(f"service {name!r}: {kind.label} entry", ref)
135
+ unknown = keys - _LONG_FORM_KEYS
136
136
  if unknown:
137
137
  msg = f"service {name!r}: unsupported {kind.label} keys {sorted(unknown)}"
138
138
  raise UnsupportedComposeError(msg)
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Topic :: Software Development :: Libraries",
19
19
  "Topic :: Software Development :: Testing",
20
20
  ]
21
- version = "0.4.0"
21
+ version = "0.4.2"
22
22
 
23
23
  [project.optional-dependencies]
24
24
  yaml = ["PyYAML>=6"]
File without changes