55import unittest
66from typing import Any
77
8+ from packaging import version
89from parameterized import parameterized
910
1011from tests .integration .test_utils import RunSubprocessMixin
12+ from tests .integration .test_utils import get_podman_version
1113from tests .integration .test_utils import podman_compose_path
1214from tests .integration .test_utils import test_path
1315
@@ -18,7 +20,7 @@ def compose_yaml_path(scenario: str) -> str:
1820 )
1921
2022
21- class TestComposeDownBehavior (unittest .TestCase , RunSubprocessMixin ):
23+ class TestComposeUpBehavior (unittest .TestCase , RunSubprocessMixin ):
2224 def get_existing_containers (self , scenario : str ) -> dict [str , Any ]:
2325 out , _ = self .run_subprocess_assert_returncode (
2426 [
@@ -133,3 +135,308 @@ def test_recreate_on_config_changed(
133135 "-t" ,
134136 "0" ,
135137 ])
138+
139+ @unittest .skipIf (
140+ get_podman_version () < version .parse ("5.6.0" ),
141+ "The image pull policy feature was only added as of Podman 5.6.0." ,
142+ )
143+ def test_pull_only_if_image_missing (self ) -> None :
144+ """Verify image is pulled, default pull policy is --missing"""
145+
146+ compose_file = compose_yaml_path ("default_pull_policy" )
147+ image = "docker.io/library/alpine:latest"
148+
149+ try :
150+ self .run_subprocess_assert_returncode ([
151+ podman_compose_path (),
152+ "-f" ,
153+ compose_file ,
154+ "up" ,
155+ "-d" ,
156+ ])
157+
158+ # Remove image while container is still up
159+ self .run_subprocess_assert_returncode ([
160+ "podman" ,
161+ "rmi" ,
162+ "-f" ,
163+ image ,
164+ ])
165+
166+ # up container one more time. Before the teardown of old container,
167+ # podman-compose finds out that the image is missing and pulls
168+ # it again so that it is immediatelly available for a new container
169+ # --missing is default pull policy
170+ _ , error = self .run_subprocess_assert_returncode ([
171+ podman_compose_path (),
172+ "--verbose" ,
173+ "-f" ,
174+ compose_file ,
175+ "up" ,
176+ "-d" ,
177+ ])
178+
179+ out = error .decode ('utf-8' )
180+ result = '\n ' .join (out .splitlines ())
181+ # podman pull command now happens before container teardown
182+ self .assertIn ("podman pull --policy missing alpine:latest" , result )
183+
184+ output , _ = self .run_subprocess_assert_returncode ([
185+ podman_compose_path (),
186+ "-f" ,
187+ compose_file ,
188+ "ps" ,
189+ ])
190+ self .assertIn (b"compose_up_behavior_test_1" , output )
191+
192+ finally :
193+ self .run_subprocess_assert_returncode ([
194+ podman_compose_path (),
195+ "-f" ,
196+ compose_file ,
197+ "down" ,
198+ "-t" ,
199+ "0" ,
200+ ])
201+ self .run_subprocess_assert_returncode ([
202+ "podman" ,
203+ "rmi" ,
204+ "-f" ,
205+ image ,
206+ ])
207+
208+ @unittest .skipIf (
209+ get_podman_version () < version .parse ("5.6.0" ),
210+ "The image pull policy feature was only added as of Podman 5.6.0." ,
211+ )
212+ def test_pull_default_policy_overrides_lower_priority_policy (self ) -> None :
213+ """Verify pull policy flag with higher priority overrides the default pull
214+ policy (--missing)"""
215+
216+ compose_file = compose_yaml_path ("override_missing" )
217+ image = "docker.io/library/alpine:latest"
218+
219+ try :
220+ self .run_subprocess_assert_returncode ([
221+ podman_compose_path (),
222+ "-f" ,
223+ compose_file ,
224+ "up" ,
225+ "-d" ,
226+ ])
227+
228+ # Remove image while container is still up
229+ self .run_subprocess_assert_returncode ([
230+ "podman" ,
231+ "rmi" ,
232+ "-f" ,
233+ image ,
234+ ])
235+
236+ # up container one more time. Before the teardown of old container,
237+ # podman-compose finds out that the pull policy of one of the images
238+ # is --always and pulls the image so that it is immediatelly available
239+ # for a new container
240+ _ , error = self .run_subprocess_assert_returncode ([
241+ podman_compose_path (),
242+ "--verbose" ,
243+ "-f" ,
244+ compose_file ,
245+ "up" ,
246+ "-d" ,
247+ ])
248+ out = error .decode ('utf-8' )
249+ result = '\n ' .join (out .splitlines ())
250+
251+ # default pull-policy is --missing, but it has been overriden by policy of
252+ # higher priority (--always)
253+ self .assertIn ("podman pull --policy always alpine:latest" , result )
254+
255+ finally :
256+ self .run_subprocess_assert_returncode ([
257+ podman_compose_path (),
258+ "-f" ,
259+ compose_file ,
260+ "down" ,
261+ "-t" ,
262+ "0" ,
263+ ])
264+ self .run_subprocess_assert_returncode ([
265+ "podman" ,
266+ "rmi" ,
267+ "-f" ,
268+ image ,
269+ ])
270+
271+ @unittest .skipIf (
272+ get_podman_version () < version .parse ("5.6.0" ),
273+ "The image pull policy feature was only added as of Podman 5.6.0." ,
274+ )
275+ def test_localhost_image_not_pulled (self ) -> None :
276+ """Verify existing localhost/ images are used locally without pulling"""
277+
278+ compose_file = compose_yaml_path ("non_existent_localhost_image" )
279+ image = "localhost/test-image:1"
280+
281+ try :
282+ # pre-create image locally by tagging a minimal base image,
283+ # this simulates the image already existing locally
284+ self .run_subprocess_assert_returncode (["podman" , "pull" , "alpine:latest" ])
285+ self .run_subprocess_assert_returncode (["podman" , "tag" , "alpine:latest" , image ])
286+
287+ output , _ = self .run_subprocess_assert_returncode ([
288+ podman_compose_path (),
289+ "-f" ,
290+ compose_file ,
291+ "up" ,
292+ "-d" ,
293+ ])
294+
295+ # Remove image while container is still up
296+ self .run_subprocess_assert_returncode ([
297+ "podman" ,
298+ "rmi" ,
299+ "-f" ,
300+ image ,
301+ ])
302+
303+ # since the compose file has both 'build' and 'image',
304+ # podman-compose should use the existing image (or rebuild if
305+ # necessary), but it should never attempt to pull localhost/ images
306+ _ , error = self .run_subprocess_assert_returncode ([
307+ podman_compose_path (),
308+ "--verbose" ,
309+ "-f" ,
310+ compose_file ,
311+ "up" ,
312+ "-d" ,
313+ ])
314+ out = error .decode ('utf-8' )
315+ result = '\n ' .join (out .splitlines ())
316+ self .assertNotIn ("Trying to pull" , result )
317+
318+ # Confirm container is actually started with localhost/ image
319+ output , _ = self .run_subprocess_assert_returncode ([
320+ "podman" ,
321+ "ps" ,
322+ "-a" ,
323+ "--filter" ,
324+ f"ancestor={ image } " ,
325+ "--format" ,
326+ "{{.Image}}" ,
327+ ])
328+ out = output .decode ('utf-8' )
329+ result = '\n ' .join (out .splitlines ())
330+ self .assertIn (image , result )
331+
332+ finally :
333+ self .run_subprocess_assert_returncode ([
334+ podman_compose_path (),
335+ "-f" ,
336+ compose_file ,
337+ "down" ,
338+ ])
339+ self .run_subprocess_assert_returncode ([
340+ "podman" ,
341+ "rmi" ,
342+ "-f" ,
343+ image ,
344+ ])
345+
346+ @unittest .skipIf (
347+ get_podman_version () < version .parse ("5.6.0" ),
348+ "The image pull policy feature was only added as of Podman 5.6.0." ,
349+ )
350+ def test_localhost_image_built_if_does_not_exist (self ) -> None :
351+ """Verify non-existent localhost/ images are built instead of pulling"""
352+
353+ compose_file = compose_yaml_path ("build_localhost_image" )
354+ image = "localhost/not-exists"
355+
356+ # check image does not exist
357+ self .run_subprocess_assert_returncode (
358+ [
359+ "podman" ,
360+ "image" ,
361+ "exists" ,
362+ image ,
363+ ],
364+ 1 ,
365+ )
366+
367+ try :
368+ # Verify image is missing before start
369+ output , _ = self .run_subprocess_assert_returncode (
370+ ["podman" , "images" , "--filter" , f"reference={ image } " , "--format" , "{{.Id}}" ],
371+ )
372+ out = output .decode ('utf-8' )
373+ result = '\n ' .join (out .splitlines ())
374+ self .assertEqual (result , "" )
375+
376+ output , _ = self .run_subprocess_assert_returncode ([
377+ podman_compose_path (),
378+ "-f" ,
379+ compose_file ,
380+ "up" ,
381+ "-d" ,
382+ ])
383+
384+ # Remove image while container is still up
385+ self .run_subprocess_assert_returncode ([
386+ "podman" ,
387+ "rmi" ,
388+ "-f" ,
389+ image ,
390+ ])
391+
392+ # image was not pulled, it was built
393+ output , _ = self .run_subprocess_assert_returncode ([
394+ podman_compose_path (),
395+ "--verbose" ,
396+ "-f" ,
397+ compose_file ,
398+ "up" ,
399+ "-d" ,
400+ ])
401+
402+ out = output .decode ('utf-8' )
403+ result = '\n ' .join (out .splitlines ())
404+ self .assertNotIn ("Trying to pull" , result )
405+
406+ # After up command, image now exists (was built)
407+ output , _ = self .run_subprocess_assert_returncode ([
408+ "podman" ,
409+ "images" ,
410+ "--filter" ,
411+ f"reference={ image } " ,
412+ "--format" ,
413+ "{{.Repository}}:{{.Tag}}" ,
414+ ])
415+ out = output .decode ('utf-8' )
416+ result = '\n ' .join (out .splitlines ())
417+ self .assertIn (image , result )
418+
419+ # Container uses the locally built image
420+ output , _ = self .run_subprocess_assert_returncode ([
421+ "podman" ,
422+ "ps" ,
423+ "-a" ,
424+ "--filter" ,
425+ f"ancestor={ image } " ,
426+ "--format" ,
427+ "{{.Image}}" ,
428+ ])
429+ out = output .decode ('utf-8' )
430+ result = '\n ' .join (out .splitlines ())
431+ self .assertIn (image , result )
432+
433+ finally :
434+ self .run_subprocess_assert_returncode ([
435+ podman_compose_path (),
436+ "-f" ,
437+ compose_file ,
438+ "down" ,
439+ ])
440+ self .run_subprocess_assert_returncode (
441+ ["podman" , "rmi" , "-f" , image ],
442+ )
0 commit comments