compose2pod 0.3.0__tar.gz → 0.4.0__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.
- {compose2pod-0.3.0 → compose2pod-0.4.0}/PKG-INFO +37 -18
- {compose2pod-0.3.0 → compose2pod-0.4.0}/README.md +36 -17
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/emit.py +20 -22
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/parsing.py +26 -8
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/pod.py +25 -19
- {compose2pod-0.3.0 → compose2pod-0.4.0}/pyproject.toml +1 -1
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/__init__.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/__main__.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/cli.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/exceptions.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/extends.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/graph.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/healthcheck.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/keys.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/py.typed +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/read.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/resources.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/shell.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/stores.py +0 -0
- {compose2pod-0.3.0 → compose2pod-0.4.0}/compose2pod/values.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: compose2pod
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -48,19 +48,18 @@ Convert a Docker Compose file into a POSIX `sh` script that runs its services as
|
|
|
48
48
|
|
|
49
49
|
Built for CI and test environments where you can't use `docker compose` or `podman kube play`:
|
|
50
50
|
|
|
51
|
-
- **No bridge networking / netavark.** Unprivileged CI containers often have a read-only `/proc/sys`, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over `127.0.0.1`, and names resolve via
|
|
51
|
+
- **No bridge networking / netavark.** Unprivileged CI containers often have a read-only `/proc/sys`, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over `127.0.0.1`, and names resolve via a generated `/etc/hosts` the script owns.
|
|
52
52
|
- **No systemd.** Podman healthchecks are normally scheduled by systemd timers. compose2pod gates startup by polling `podman healthcheck run` directly, so `depends_on: service_healthy` works without systemd.
|
|
53
53
|
- **No heavy runtime.** The core is stdlib-only — no dependencies, no compiled wheels — so it installs and runs in minimal Python images.
|
|
54
54
|
|
|
55
55
|
## Requirements
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
name resolution for everything started after it on a pre-6.0.0 Podman.
|
|
57
|
+
compose2pod's generated scripts own `/etc/hosts`: they write it to a temp
|
|
58
|
+
file and bind-mount it read-only into every container under `--no-hosts`
|
|
59
|
+
(see `architecture/supported-subset.md`), so pod-internal name resolution
|
|
60
|
+
works on any Podman version. `host.containers.internal` /
|
|
61
|
+
`host.docker.internal` are not provided — add an explicit `extra_hosts`
|
|
62
|
+
entry if you need them.
|
|
64
63
|
|
|
65
64
|
## Install
|
|
66
65
|
|
|
@@ -83,15 +82,35 @@ sh ./run.sh
|
|
|
83
82
|
|
|
84
83
|
## Supported compose subset
|
|
85
84
|
|
|
86
|
-
compose2pod
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
compose2pod refuses **every document `docker compose config` refuses** — a
|
|
86
|
+
measured property, checked continuously by a differential conformance harness
|
|
87
|
+
that runs the real Docker CLI and the real compose2pod pipeline over the same
|
|
88
|
+
YAML. So a file that compiles is a file Docker would run; where compose2pod
|
|
89
|
+
still refuses a form Docker accepts, it is because Podman genuinely cannot
|
|
90
|
+
express it (each such case is documented, not guessed).
|
|
91
|
+
|
|
92
|
+
Within that boundary it covers most of what real compose files use:
|
|
93
|
+
|
|
94
|
+
- **Services** — `image`/`build`, `command`/`entrypoint`, `environment` and
|
|
95
|
+
`env_file` (string and long-form `{path, required, format}`), `volumes`
|
|
96
|
+
(short-form and long-form `--mount`, including the `bind`/`volume`/`tmpfs`
|
|
97
|
+
option maps), `tmpfs`, `healthcheck`, `depends_on` (all conditions), network
|
|
98
|
+
`aliases`, `hostname`/`container_name`.
|
|
99
|
+
- **Confinement & metadata** — `user`, `working_dir`, `read_only`, `init`,
|
|
100
|
+
`privileged`, `cap_add`/`cap_drop`, `security_opt`, `devices`, `group_add`,
|
|
101
|
+
`platform`, `labels`, `annotations`, `pull_policy` (the quoted-boolean and
|
|
102
|
+
YAML-1.1 spellings Docker accepts, too).
|
|
103
|
+
- **Resources** — the legacy keys (`mem_limit`, `cpus`, `pids_limit`,
|
|
104
|
+
`ulimits`, …) and the modern `deploy.resources` block.
|
|
105
|
+
- **Pod-wide** — `dns`/`dns_search`/`dns_opt`, `sysctls`, `extra_hosts`.
|
|
106
|
+
- **Composition** — same-file `extends`, `secrets`/`configs`, `profiles`.
|
|
107
|
+
|
|
108
|
+
Compose extension fields (any `x-`-prefixed key) and YAML anchors are accepted
|
|
109
|
+
as-is, so a top-level `x-*` anchor block for shared config just works.
|
|
110
|
+
`${VAR}`-style variable interpolation is left live in the generated script,
|
|
111
|
+
resolved by its shell against the environment present when the script runs (no
|
|
112
|
+
`.env` file support). See `architecture/supported-subset.md` for the full
|
|
113
|
+
accept/ignore/reject matrix and `planning/decisions/` for the boundary rulings.
|
|
95
114
|
|
|
96
115
|
## Status
|
|
97
116
|
|
|
@@ -22,19 +22,18 @@ Convert a Docker Compose file into a POSIX `sh` script that runs its services as
|
|
|
22
22
|
|
|
23
23
|
Built for CI and test environments where you can't use `docker compose` or `podman kube play`:
|
|
24
24
|
|
|
25
|
-
- **No bridge networking / netavark.** Unprivileged CI containers often have a read-only `/proc/sys`, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over `127.0.0.1`, and names resolve via
|
|
25
|
+
- **No bridge networking / netavark.** Unprivileged CI containers often have a read-only `/proc/sys`, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over `127.0.0.1`, and names resolve via a generated `/etc/hosts` the script owns.
|
|
26
26
|
- **No systemd.** Podman healthchecks are normally scheduled by systemd timers. compose2pod gates startup by polling `podman healthcheck run` directly, so `depends_on: service_healthy` works without systemd.
|
|
27
27
|
- **No heavy runtime.** The core is stdlib-only — no dependencies, no compiled wheels — so it installs and runs in minimal Python images.
|
|
28
28
|
|
|
29
29
|
## Requirements
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
name resolution for everything started after it on a pre-6.0.0 Podman.
|
|
31
|
+
compose2pod's generated scripts own `/etc/hosts`: they write it to a temp
|
|
32
|
+
file and bind-mount it read-only into every container under `--no-hosts`
|
|
33
|
+
(see `architecture/supported-subset.md`), so pod-internal name resolution
|
|
34
|
+
works on any Podman version. `host.containers.internal` /
|
|
35
|
+
`host.docker.internal` are not provided — add an explicit `extra_hosts`
|
|
36
|
+
entry if you need them.
|
|
38
37
|
|
|
39
38
|
## Install
|
|
40
39
|
|
|
@@ -57,15 +56,35 @@ sh ./run.sh
|
|
|
57
56
|
|
|
58
57
|
## Supported compose subset
|
|
59
58
|
|
|
60
|
-
compose2pod
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
compose2pod refuses **every document `docker compose config` refuses** — a
|
|
60
|
+
measured property, checked continuously by a differential conformance harness
|
|
61
|
+
that runs the real Docker CLI and the real compose2pod pipeline over the same
|
|
62
|
+
YAML. So a file that compiles is a file Docker would run; where compose2pod
|
|
63
|
+
still refuses a form Docker accepts, it is because Podman genuinely cannot
|
|
64
|
+
express it (each such case is documented, not guessed).
|
|
65
|
+
|
|
66
|
+
Within that boundary it covers most of what real compose files use:
|
|
67
|
+
|
|
68
|
+
- **Services** — `image`/`build`, `command`/`entrypoint`, `environment` and
|
|
69
|
+
`env_file` (string and long-form `{path, required, format}`), `volumes`
|
|
70
|
+
(short-form and long-form `--mount`, including the `bind`/`volume`/`tmpfs`
|
|
71
|
+
option maps), `tmpfs`, `healthcheck`, `depends_on` (all conditions), network
|
|
72
|
+
`aliases`, `hostname`/`container_name`.
|
|
73
|
+
- **Confinement & metadata** — `user`, `working_dir`, `read_only`, `init`,
|
|
74
|
+
`privileged`, `cap_add`/`cap_drop`, `security_opt`, `devices`, `group_add`,
|
|
75
|
+
`platform`, `labels`, `annotations`, `pull_policy` (the quoted-boolean and
|
|
76
|
+
YAML-1.1 spellings Docker accepts, too).
|
|
77
|
+
- **Resources** — the legacy keys (`mem_limit`, `cpus`, `pids_limit`,
|
|
78
|
+
`ulimits`, …) and the modern `deploy.resources` block.
|
|
79
|
+
- **Pod-wide** — `dns`/`dns_search`/`dns_opt`, `sysctls`, `extra_hosts`.
|
|
80
|
+
- **Composition** — same-file `extends`, `secrets`/`configs`, `profiles`.
|
|
81
|
+
|
|
82
|
+
Compose extension fields (any `x-`-prefixed key) and YAML anchors are accepted
|
|
83
|
+
as-is, so a top-level `x-*` anchor block for shared config just works.
|
|
84
|
+
`${VAR}`-style variable interpolation is left live in the generated script,
|
|
85
|
+
resolved by its shell against the environment present when the script runs (no
|
|
86
|
+
`.env` file support). See `architecture/supported-subset.md` for the full
|
|
87
|
+
accept/ignore/reject matrix and `planning/decisions/` for the boundary rulings.
|
|
69
88
|
|
|
70
89
|
## Status
|
|
71
90
|
|
|
@@ -12,7 +12,7 @@ from compose2pod.graph import depends_on, hostnames, startup_order
|
|
|
12
12
|
from compose2pod.healthcheck import health_cmd, interval_seconds
|
|
13
13
|
from compose2pod.keys import SERVICE_KEYS, Expand, GuardedEnvFile, Token
|
|
14
14
|
from compose2pod.parsing import validate
|
|
15
|
-
from compose2pod.pod import pod_create_flags
|
|
15
|
+
from compose2pod.pod import hosts_file_tokens, pod_create_flags
|
|
16
16
|
from compose2pod.resources import deploy_resource_flags
|
|
17
17
|
from compose2pod.shell import to_shell, variable_names
|
|
18
18
|
from compose2pod.values import as_bool
|
|
@@ -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
|
|
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
|
|
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, {}))
|
|
@@ -161,23 +161,16 @@ def run_flags(name: str, svc: dict[str, Any], pod: str, project_dir: str) -> lis
|
|
|
161
161
|
return flags
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
# Every container mounts the script-owned /etc/hosts read-only. `--no-hosts`
|
|
165
|
+
# stops podman managing the file (so the pre-6.0.0 stop-wipe never runs); `z`
|
|
166
|
+
# relabels for SELinux (mandatory on Fedora, a no-op elsewhere).
|
|
167
|
+
_HOSTS_MOUNT = '--no-hosts -v "$hostsfile":/etc/hosts:ro,z'
|
|
168
|
+
|
|
164
169
|
_SCRIPT_HEADER = """\
|
|
165
170
|
#!/bin/sh
|
|
166
171
|
# Generated by compose2pod -- do not edit, regenerate instead.
|
|
167
172
|
set -eu
|
|
168
173
|
|
|
169
|
-
podman_version=$(podman version --format '{{.Client.Version}}' 2>/dev/null) || podman_version="unknown"
|
|
170
|
-
podman_major=$(echo "$podman_version" | cut -d. -f1)
|
|
171
|
-
case "$podman_major" in
|
|
172
|
-
''|*[!0-9]*) ;; # unparseable -- skip rather than false-positive
|
|
173
|
-
*) if [ "$podman_major" -lt 6 ]; then
|
|
174
|
-
echo "warning: podman $podman_version detected; compose2pod requires podman >= 6.0.0" >&2
|
|
175
|
-
echo "warning: podman < 6.0.0 has a bug where a container stopping in a multi-container" >&2
|
|
176
|
-
echo "pod wipes /etc/hosts for the whole pod (fixed in 6.0.0) -- name resolution between" >&2
|
|
177
|
-
echo "services may fail unpredictably" >&2
|
|
178
|
-
fi ;;
|
|
179
|
-
esac
|
|
180
|
-
|
|
181
174
|
wait_healthy() {
|
|
182
175
|
ctr=$1
|
|
183
176
|
attempts=$2
|
|
@@ -277,7 +270,7 @@ def _emit_target(lines: list[str], tokens: list[Token], options: EmitOptions) ->
|
|
|
277
270
|
target_ctr = shlex.quote(f"{options.pod}-{options.target}")
|
|
278
271
|
lines.append("rc=0")
|
|
279
272
|
lines.extend(_env_file_preludes(tokens))
|
|
280
|
-
lines.append(f"podman run {_render(tokens)} || rc=$?")
|
|
273
|
+
lines.append(f"podman run {_HOSTS_MOUNT} {_render(tokens)} || rc=$?")
|
|
281
274
|
for artifact in options.artifacts:
|
|
282
275
|
source, destination = artifact.split(":", 1)
|
|
283
276
|
lines.append(f"podman cp {target_ctr}:{shlex.quote(source)} {shlex.quote(destination)} || true")
|
|
@@ -343,6 +336,7 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript:
|
|
|
343
336
|
# pointing at 127.0.0.1 for a service that never runs would turn an honest
|
|
344
337
|
# resolution failure into a connection-refused.
|
|
345
338
|
hosts = hostnames({name: services[name] for name in order})
|
|
339
|
+
host_tokens = hosts_file_tokens(services, order, hosts)
|
|
346
340
|
completion_gated = {
|
|
347
341
|
dep
|
|
348
342
|
for svc in services.values()
|
|
@@ -352,17 +346,21 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript:
|
|
|
352
346
|
names: set[str] = set()
|
|
353
347
|
|
|
354
348
|
lines = [_SCRIPT_HEADER]
|
|
349
|
+
lines.append("hostsfile=$(mktemp)")
|
|
355
350
|
teardown = f"podman pod rm -f {shlex.quote(options.pod)} >/dev/null 2>&1 || true"
|
|
356
351
|
store_teardown = stores.teardown_line(compose, order, options.pod)
|
|
357
352
|
if store_teardown:
|
|
358
353
|
teardown += f"; {store_teardown}"
|
|
354
|
+
teardown += '; rm -f "$hostsfile"'
|
|
359
355
|
lines.append(f"trap '{teardown}' EXIT")
|
|
360
|
-
pod_flags = pod_create_flags(services, order
|
|
356
|
+
pod_flags = pod_create_flags(services, order)
|
|
361
357
|
_collect_vars(pod_flags, names)
|
|
362
|
-
pod_create = f"podman pod create --name {shlex.quote(options.pod)}"
|
|
358
|
+
pod_create = f"podman pod create --name {shlex.quote(options.pod)} --no-hosts"
|
|
363
359
|
if pod_flags:
|
|
364
360
|
pod_create += " " + _render(pod_flags)
|
|
365
361
|
lines.append(pod_create)
|
|
362
|
+
_collect_vars(host_tokens, names)
|
|
363
|
+
lines.append(f"printf '%s\\n' {_render(host_tokens)} > \"$hostsfile\"")
|
|
366
364
|
lines.extend(stores.create_lines(compose, order, options.pod, options.project_dir))
|
|
367
365
|
names |= stores.referenced_variables(compose, order, options.project_dir)
|
|
368
366
|
waited: set[str] = set()
|
|
@@ -379,10 +377,10 @@ def _plan(compose: dict[str, Any], options: EmitOptions) -> PlannedScript:
|
|
|
379
377
|
_emit_target(lines, tokens, options)
|
|
380
378
|
elif name in completion_gated:
|
|
381
379
|
lines.extend(_env_file_preludes(tokens))
|
|
382
|
-
lines.append(f"podman run --rm {_render(tokens)}")
|
|
380
|
+
lines.append(f"podman run --rm {_HOSTS_MOUNT} {_render(tokens)}")
|
|
383
381
|
else:
|
|
384
382
|
lines.extend(_env_file_preludes(tokens))
|
|
385
|
-
lines.append(f"podman run -d {_render(tokens)}")
|
|
383
|
+
lines.append(f"podman run -d {_HOSTS_MOUNT} {_render(tokens)}")
|
|
386
384
|
return PlannedScript(script="\n".join(lines) + "\n", variables=sorted(names))
|
|
387
385
|
|
|
388
386
|
|
|
@@ -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,
|
|
209
|
-
one nested option map matching `type` (a mismatched sub-map is
|
|
210
|
-
a deliberate stricter-than-docker check; docker
|
|
211
|
-
cluster/npipe
|
|
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
|
|
328
|
-
if vtype
|
|
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}:
|
|
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:
|
|
@@ -47,8 +47,8 @@ def _check_extra_host_separators(name: str, value: Any) -> None: # noqa: ANN401
|
|
|
47
47
|
"""Check each list-form entry actually divides into a host and an address.
|
|
48
48
|
|
|
49
49
|
An entry with neither separator has no address to emit, and would render as
|
|
50
|
-
a malformed
|
|
51
|
-
problem: its key and value are already separate.
|
|
50
|
+
a malformed hosts-file line (a name with no address). The mapping form cannot
|
|
51
|
+
have this problem: its key and value are already separate.
|
|
52
52
|
"""
|
|
53
53
|
if not isinstance(value, list):
|
|
54
54
|
return
|
|
@@ -65,7 +65,7 @@ def _check_extra_host_value_types(name: str, value: Any) -> None: # noqa: ANN40
|
|
|
65
65
|
number, boolean, or null -- correct for `labels`/`annotations`' bare-key-
|
|
66
66
|
means-null semantics, but too loose for `extra_hosts`: an int/bool/null
|
|
67
67
|
address would otherwise reach `keys.extra_host_entries` and get coerced
|
|
68
|
-
into a bogus
|
|
68
|
+
into a bogus hosts-file line. Measured against `docker compose config`
|
|
69
69
|
v5.1.2: `extra_hosts: {h: 3}` and `extra_hosts: {h: true}` are both
|
|
70
70
|
refused ("services.<svc>.extra_hosts.h must be a string"). The list form
|
|
71
71
|
cannot have this problem: `validate_map` already requires every list
|
|
@@ -131,14 +131,19 @@ def _sysctl_flags(services: dict[str, Any], order: list[str]) -> list[Token]:
|
|
|
131
131
|
return tokens
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
"""Merge alias/hostname hosts (fixed 127.0.0.1) with extra_hosts (order-scoped) into one add-host set.
|
|
134
|
+
_LOCALHOST_LINES: tuple[str, str] = ("127.0.0.1 localhost", "::1 localhost")
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
the
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
|
|
137
|
+
def hosts_file_tokens(services: dict[str, Any], order: list[str], hosts: list[str]) -> list[Token]:
|
|
138
|
+
"""Render the pod's /etc/hosts as `IP NAME` line tokens (localhost lines first).
|
|
139
|
+
|
|
140
|
+
compose2pod owns /etc/hosts: the generated script bind-mounts these lines
|
|
141
|
+
into every container under `--no-hosts`, replacing the former pod-level
|
|
142
|
+
`--add-host` set (which conflicts with `--no-hosts`). Merge/conflict rules
|
|
143
|
+
are unchanged from the add-host era: alias/hostname names are fixed at
|
|
144
|
+
127.0.0.1, `extra_hosts` is order-scoped, and a name landing on two
|
|
145
|
+
addresses is refused. Alias lines are literal tokens; `extra_hosts` lines
|
|
146
|
+
render via `Expand` so a `${VAR}` address stays live at run time.
|
|
142
147
|
"""
|
|
143
148
|
merged: dict[str, str] = {}
|
|
144
149
|
from_extra_hosts: set[str] = set()
|
|
@@ -154,18 +159,19 @@ def _add_host_flags(services: dict[str, Any], order: list[str], hosts: list[str]
|
|
|
154
159
|
raise UnsupportedComposeError(msg)
|
|
155
160
|
merged[host] = addr
|
|
156
161
|
from_extra_hosts.add(host)
|
|
157
|
-
tokens: list[Token] = []
|
|
162
|
+
tokens: list[Token] = [*_LOCALHOST_LINES]
|
|
158
163
|
for host, addr in merged.items():
|
|
159
|
-
|
|
160
|
-
tokens
|
|
164
|
+
line = f"{addr} {host}"
|
|
165
|
+
tokens.append(Expand(value=line) if host in from_extra_hosts else line)
|
|
161
166
|
return tokens
|
|
162
167
|
|
|
163
168
|
|
|
164
|
-
def pod_create_flags(services: dict[str, Any], order: list[str]
|
|
165
|
-
"""Pod-create flag tokens aggregated across the closure `order
|
|
169
|
+
def pod_create_flags(services: dict[str, Any], order: list[str]) -> list[Token]:
|
|
170
|
+
"""Pod-create flag tokens aggregated across the closure `order`.
|
|
166
171
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
dns/dns_search/dns_opt are unioned (dedup, first-seen order); sysctls are
|
|
173
|
+
unioned by key. Name resolution no longer rides on `--add-host` (it conflicts
|
|
174
|
+
with the `--no-hosts` the script now passes); it lives in the bind-mounted
|
|
175
|
+
hosts file -- see `hosts_file_tokens`.
|
|
170
176
|
"""
|
|
171
|
-
return
|
|
177
|
+
return _dns_flags(services, order) + _sysctl_flags(services, order)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|