Skip to content

Commit 87d065d

Browse files
authored
Merge pull request #1263 from uosis/no-recreate
Implement `up --no-recreate`
2 parents 406596e + 7a0c6e7 commit 87d065d

File tree

7 files changed

+147
-32
lines changed

7 files changed

+147
-32
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implemented `up --no-recreate` to work as advertised

podman_compose.py

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3099,6 +3099,8 @@ def deps_from_container(args: argparse.Namespace, cnt: dict) -> set:
30993099
async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int | None:
31003100
excluded = get_excluded(compose, args)
31013101

3102+
log.info("building images: ...")
3103+
31023104
if not args.no_build:
31033105
# `podman build` does not cache, so don't always build
31043106
build_args = argparse.Namespace(if_not_exists=(not args.build), **args.__dict__)
@@ -3108,8 +3110,11 @@ async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int |
31083110
if not args.dry_run:
31093111
return build_exit_code
31103112

3111-
hashes = (
3112-
(
3113+
# if needed, tear down existing containers
3114+
3115+
existing_containers: dict[str, str | None] = {
3116+
c['Names'][0]: c['Labels'].get('io.podman.compose.config-hash')
3117+
for c in json.loads(
31133118
await compose.podman.output(
31143119
[],
31153120
"ps",
@@ -3118,44 +3123,73 @@ async def compose_up(compose: PodmanCompose, args: argparse.Namespace) -> int |
31183123
f"label=io.podman.compose.project={compose.project_name}",
31193124
"-a",
31203125
"--format",
3121-
'{{ index .Labels "io.podman.compose.config-hash"}}',
3126+
"json",
31223127
],
31233128
)
31243129
)
3125-
.decode("utf-8")
3126-
.splitlines()
3127-
)
3128-
diff_hashes = [i for i in hashes if i and i != compose.yaml_hash]
3129-
if (args.force_recreate and len(hashes) > 0) or len(diff_hashes):
3130-
log.info("recreating: ...")
3131-
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None))
3132-
await compose.commands["down"](compose, down_args)
3133-
log.info("recreating: done\n\n")
3134-
# args.no_recreate disables check for changes (which is not implemented)
3130+
}
3131+
3132+
if len(existing_containers) > 0:
3133+
if args.force_recreate and args.no_recreate:
3134+
log.error(
3135+
"Cannot use --force-recreate and --no-recreate at the same time, "
3136+
"please remove one of them"
3137+
)
3138+
return 1
3139+
3140+
if args.force_recreate:
3141+
teardown_needed = True
3142+
elif args.no_recreate:
3143+
teardown_needed = False
3144+
else:
3145+
# default is to tear down everything if any container is stale
3146+
teardown_needed = (
3147+
len([h for h in existing_containers.values() if h != compose.yaml_hash]) > 0
3148+
)
3149+
3150+
if teardown_needed:
3151+
log.info("tearing down existing containers: ...")
3152+
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None))
3153+
await compose.commands["down"](compose, down_args)
3154+
existing_containers = {}
3155+
log.info("tearing down existing containers: done\n\n")
31353156

31363157
await create_pods(compose)
3137-
exit_code = 0
3158+
3159+
log.info("creating missing containers: ...")
3160+
3161+
create_error_codes: list[int | None] = []
31383162
for cnt in compose.containers:
3139-
if cnt["_service"] in excluded:
3140-
log.debug("** skipping: %s", cnt["name"])
3163+
if cnt["_service"] in excluded or cnt["name"] in existing_containers:
3164+
log.debug("** skipping create: %s", cnt["name"])
31413165
continue
31423166
podman_args = await container_to_args(compose, cnt, detached=False, no_deps=args.no_deps)
3143-
subproc_exit_code = await compose.podman.run([], "create", podman_args)
3144-
if subproc_exit_code is not None and subproc_exit_code != 0:
3145-
exit_code = subproc_exit_code
3167+
exit_code = await compose.podman.run([], "create", podman_args)
3168+
create_error_codes.append(exit_code)
3169+
3170+
if args.dry_run:
3171+
return None
31463172

3147-
if not args.no_start and args.detach and subproc_exit_code is not None:
3148-
container_exit_code = await run_container(
3173+
if args.no_start:
3174+
# return first error code from create calls, if any
3175+
return next((code for code in create_error_codes if code is not None and code != 0), 0)
3176+
3177+
if args.detach:
3178+
log.info("starting containers (detached): ...")
3179+
start_error_codes: list[int | None] = []
3180+
for cnt in compose.containers:
3181+
if cnt["_service"] in excluded:
3182+
log.debug("** skipping start: %s", cnt["name"])
3183+
continue
3184+
exit_code = await run_container(
31493185
compose, cnt["name"], deps_from_container(args, cnt), ([], "start", [cnt["name"]])
31503186
)
3187+
start_error_codes.append(exit_code)
31513188

3152-
if container_exit_code is not None and container_exit_code != 0:
3153-
exit_code = container_exit_code
3189+
# return first error code from start calls, if any
3190+
return next((code for code in start_error_codes if code is not None and code != 0), 0)
31543191

3155-
if args.dry_run:
3156-
return None
3157-
if args.no_start or args.detach:
3158-
return exit_code
3192+
log.info("starting containers (attached): ...")
31593193

31603194
# TODO: handle already existing
31613195
# TODO: if error creating do not enter loop
@@ -3393,6 +3427,7 @@ async def compose_run(compose: PodmanCompose, args: argparse.Namespace) -> None:
33933427
no_build=False,
33943428
build=None,
33953429
force_recreate=False,
3430+
no_recreate=False,
33963431
no_start=False,
33973432
no_cache=False,
33983433
build_arg=[],

tests/integration/no_recreate/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
services:
2+
web:
3+
image: busybox
4+
command: ["/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8004"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
services:
2+
web:
3+
image: busybox
4+
command: ["/bin/busybox", "httpd", "-f", "-h", ".", "-p", "8005"]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import unittest
5+
6+
from tests.integration.test_utils import RunSubprocessMixin
7+
from tests.integration.test_utils import podman_compose_path
8+
from tests.integration.test_utils import test_path
9+
10+
11+
def compose_yaml_path(version: str = '1') -> str:
12+
return os.path.join(os.path.join(test_path(), "no_recreate"), f"compose{version}.yaml")
13+
14+
15+
class TestComposeNoRecreate(unittest.TestCase, RunSubprocessMixin):
16+
def test_no_recreate(self) -> None:
17+
def up(args: list[str] = [], version: str = '1') -> None:
18+
self.run_subprocess_assert_returncode(
19+
[
20+
podman_compose_path(),
21+
"-f",
22+
compose_yaml_path(version),
23+
"up",
24+
"-t",
25+
"0",
26+
"-d",
27+
*args,
28+
],
29+
)
30+
31+
def get_container_id() -> bytes:
32+
return self.run_subprocess_assert_returncode([
33+
podman_compose_path(),
34+
"-f",
35+
compose_yaml_path(),
36+
"ps",
37+
"--format",
38+
'{{.ID}}',
39+
])[0]
40+
41+
try:
42+
up()
43+
44+
container_id = get_container_id()
45+
46+
self.assertGreater(len(container_id), 0)
47+
48+
# Default behavior - up with same compose file should not recreate the container
49+
up()
50+
self.assertEqual(get_container_id(), container_id)
51+
52+
# Default behavior - up with modified compose file should recreate the container
53+
up(version='2')
54+
self.assertNotEqual(get_container_id(), container_id)
55+
56+
container_id = get_container_id()
57+
58+
# Using --no-recreate should not recreate the container
59+
# even if the compose file is modified
60+
up(["--no-recreate"], version='1')
61+
self.assertEqual(get_container_id(), container_id)
62+
63+
# Using --force-recreate should recreate the container
64+
# even if the compose file is not modified
65+
up(["--force-recreate"], version='1')
66+
self.assertNotEqual(get_container_id(), container_id)
67+
finally:
68+
self.run_subprocess_assert_returncode([
69+
podman_compose_path(),
70+
"-f",
71+
compose_yaml_path(),
72+
"down",
73+
"-t",
74+
"0",
75+
])

tests/integration/service_scale/test_podman_compose_scale.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,7 @@ def test_scaleup_cli(self) -> None:
110110
"--scale",
111111
"service1=4",
112112
])
113-
# error code 125 is expected as podman-compose complains about already used name
114-
# "podman-compose_service1_1" for the 1st container
115-
# Nevertheless, following containers are still created to scale as expected
116-
# (in test case till 3 containers)
117-
self.assertEqual(return_code, 125)
113+
self.assertEqual(return_code, 0)
118114

119115
output, _, return_code = self.run_subprocess([
120116
podman_compose_path(),

0 commit comments

Comments
 (0)