@@ -32,7 +32,7 @@ def setUp(self):
3232 profile = None ,
3333 )
3434
35- @patch ("samcli.commands .package.package_context .get_validated_container_client" )
35+ @patch ("samcli.lib .package.ecr_uploader .get_validated_container_client" )
3636 @patch .object (SamLocalStackProvider , "get_stacks" )
3737 @patch .object (Template , "export" , MagicMock (sideeffect = OSError ))
3838 @patch ("boto3.client" )
@@ -46,7 +46,7 @@ def test_template_permissions_error(self, patched_boto, patched_get_stacks, mock
4646 with patch .object (self .package_command_context , "_warn_preview_runtime" ) as patched_warn_preview_runtime :
4747 self .package_command_context .run ()
4848
49- @patch ("samcli.commands .package.package_context .get_validated_container_client" )
49+ @patch ("samcli.lib .package.ecr_uploader .get_validated_container_client" )
5050 @patch .object (ResourceMetadataNormalizer , "normalize" , MagicMock ())
5151 @patch .object (Template , "export" , MagicMock (return_value = {}))
5252 @patch ("boto3.client" )
@@ -74,7 +74,7 @@ def test_template_path_valid_with_output_template(self, patched_boto, mock_get_v
7474 )
7575 package_command_context .run ()
7676
77- @patch ("samcli.commands .package.package_context .get_validated_container_client" )
77+ @patch ("samcli.lib .package.ecr_uploader .get_validated_container_client" )
7878 @patch .object (ResourceMetadataNormalizer , "normalize" , MagicMock ())
7979 @patch .object (Template , "export" , MagicMock (return_value = {}))
8080 @patch ("boto3.client" )
@@ -101,7 +101,7 @@ def test_template_path_valid(self, patched_boto, mock_get_validated_client):
101101 )
102102 package_command_context .run ()
103103
104- @patch ("samcli.commands .package.package_context .get_validated_container_client" )
104+ @patch ("samcli.lib .package.ecr_uploader .get_validated_container_client" )
105105 @patch .object (ResourceMetadataNormalizer , "normalize" , MagicMock ())
106106 @patch .object (Template , "export" , MagicMock (return_value = {}))
107107 @patch ("boto3.client" )
@@ -128,7 +128,7 @@ def test_template_path_valid_no_json(self, patched_boto, mock_get_validated_clie
128128 )
129129 package_command_context .run ()
130130
131- @patch ("samcli.commands .package.package_context .get_validated_container_client" )
131+ @patch ("samcli.lib .package.ecr_uploader .get_validated_container_client" )
132132 @patch ("samcli.commands.package.package_context.PackageContext._warn_preview_runtime" )
133133 @patch ("samcli.commands.package.package_context.get_resource_full_path_by_id" )
134134 @patch .object (SamLocalStackProvider , "get_stacks" )
@@ -214,3 +214,176 @@ def test_warn_preview_runtime(self, runtime, should_warn, function_type, patched
214214 patched_click .secho .assert_called_once ()
215215 else :
216216 patched_click .secho .assert_not_called ()
217+
218+
219+ class TestPackageContextDockerLazyInitialization (TestCase ):
220+ """Test cases for lazy Docker client initialization in PackageContext"""
221+
222+ def setUp (self ):
223+ self .package_command_context = PackageContext (
224+ template_file = "template-file" ,
225+ s3_bucket = "s3-bucket" ,
226+ s3_prefix = "s3-prefix" ,
227+ image_repository = "image-repo" ,
228+ image_repositories = None ,
229+ kms_key_id = "kms-key-id" ,
230+ output_template_file = None ,
231+ use_json = True ,
232+ force_upload = True ,
233+ no_progressbar = False ,
234+ metadata = {},
235+ region = None ,
236+ profile = None ,
237+ )
238+
239+ @patch ("samcli.lib.package.ecr_uploader.get_validated_container_client" )
240+ @patch .object (ResourceMetadataNormalizer , "normalize" , MagicMock ())
241+ @patch .object (Template , "export" , MagicMock (return_value = {}))
242+ @patch ("boto3.client" )
243+ def test_docker_client_not_validated_during_package_context_run (self , patched_boto , mock_get_validated_client ):
244+ """Test that Docker client is not validated during PackageContext.run()"""
245+ mock_docker_client = Mock ()
246+ mock_get_validated_client .return_value = mock_docker_client
247+
248+ with tempfile .NamedTemporaryFile (mode = "w" , delete = False ) as temp_template_file :
249+ package_command_context = PackageContext (
250+ template_file = temp_template_file .name ,
251+ s3_bucket = "s3-bucket" ,
252+ s3_prefix = "s3-prefix" ,
253+ image_repository = "image-repo" ,
254+ image_repositories = None ,
255+ kms_key_id = "kms-key-id" ,
256+ output_template_file = None ,
257+ use_json = True ,
258+ force_upload = True ,
259+ no_progressbar = False ,
260+ metadata = {},
261+ region = "us-east-2" ,
262+ profile = None ,
263+ )
264+
265+ # Run the package context
266+ package_command_context .run ()
267+
268+ # Docker client validation should not have been called during run()
269+ mock_get_validated_client .assert_not_called ()
270+
271+ # ECR uploader should be created but Docker client not validated yet
272+ ecr_uploader = package_command_context .uploaders .ecr
273+ self .assertIsNone (ecr_uploader ._validated_docker_client )
274+
275+ @patch ("samcli.lib.package.ecr_uploader.get_validated_container_client" )
276+ @patch .object (ResourceMetadataNormalizer , "normalize" , MagicMock ())
277+ @patch .object (Template , "export" , MagicMock (return_value = {}))
278+ @patch ("boto3.client" )
279+ def test_docker_client_validated_only_when_ecr_operations_performed (self , patched_boto , mock_get_validated_client ):
280+ """Test that Docker client is validated only when ECR operations are performed"""
281+ mock_docker_client = Mock ()
282+ mock_get_validated_client .return_value = mock_docker_client
283+
284+ with tempfile .NamedTemporaryFile (mode = "w" , delete = False ) as temp_template_file :
285+ package_command_context = PackageContext (
286+ template_file = temp_template_file .name ,
287+ s3_bucket = "s3-bucket" ,
288+ s3_prefix = "s3-prefix" ,
289+ image_repository = "image-repo" ,
290+ image_repositories = None ,
291+ kms_key_id = "kms-key-id" ,
292+ output_template_file = None ,
293+ use_json = True ,
294+ force_upload = True ,
295+ no_progressbar = False ,
296+ metadata = {},
297+ region = "us-east-2" ,
298+ profile = None ,
299+ )
300+
301+ # Run the package context
302+ package_command_context .run ()
303+
304+ # Docker client validation should not have been called yet
305+ mock_get_validated_client .assert_not_called ()
306+
307+ # Access the docker_client property to trigger validation
308+ ecr_uploader = package_command_context .uploaders .ecr
309+ docker_client = ecr_uploader .docker_client
310+
311+ # Now Docker client validation should have been called
312+ mock_get_validated_client .assert_called_once ()
313+ self .assertEqual (docker_client , mock_docker_client )
314+
315+ @patch ("samcli.lib.package.ecr_uploader.get_validated_container_client" )
316+ @patch .object (ResourceMetadataNormalizer , "normalize" , MagicMock ())
317+ @patch .object (Template , "export" , MagicMock (return_value = {}))
318+ @patch ("boto3.client" )
319+ def test_ecr_uploader_created_with_none_docker_client (self , patched_boto , mock_get_validated_client ):
320+ """Test that ECRUploader is created with None docker_client parameter"""
321+ mock_docker_client = Mock ()
322+ mock_get_validated_client .return_value = mock_docker_client
323+
324+ with tempfile .NamedTemporaryFile (mode = "w" , delete = False ) as temp_template_file :
325+ package_command_context = PackageContext (
326+ template_file = temp_template_file .name ,
327+ s3_bucket = "s3-bucket" ,
328+ s3_prefix = "s3-prefix" ,
329+ image_repository = "image-repo" ,
330+ image_repositories = None ,
331+ kms_key_id = "kms-key-id" ,
332+ output_template_file = None ,
333+ use_json = True ,
334+ force_upload = True ,
335+ no_progressbar = False ,
336+ metadata = {},
337+ region = "us-east-2" ,
338+ profile = None ,
339+ )
340+
341+ # Run the package context
342+ package_command_context .run ()
343+
344+ # Verify ECRUploader was created with None docker_client parameter
345+ ecr_uploader = package_command_context .uploaders .ecr
346+ self .assertIsNone (ecr_uploader ._docker_client_param )
347+ self .assertIsNone (ecr_uploader ._validated_docker_client )
348+
349+ @patch ("samcli.lib.package.ecr_uploader.get_validated_container_client" )
350+ @patch .object (ResourceMetadataNormalizer , "normalize" , MagicMock ())
351+ @patch .object (Template , "export" , MagicMock (return_value = {}))
352+ @patch ("boto3.client" )
353+ def test_multiple_docker_client_accesses_only_validate_once (self , patched_boto , mock_get_validated_client ):
354+ """Test that multiple accesses to docker_client only validate once"""
355+ mock_docker_client = Mock ()
356+ mock_get_validated_client .return_value = mock_docker_client
357+
358+ with tempfile .NamedTemporaryFile (mode = "w" , delete = False ) as temp_template_file :
359+ package_command_context = PackageContext (
360+ template_file = temp_template_file .name ,
361+ s3_bucket = "s3-bucket" ,
362+ s3_prefix = "s3-prefix" ,
363+ image_repository = "image-repo" ,
364+ image_repositories = None ,
365+ kms_key_id = "kms-key-id" ,
366+ output_template_file = None ,
367+ use_json = True ,
368+ force_upload = True ,
369+ no_progressbar = False ,
370+ metadata = {},
371+ region = "us-east-2" ,
372+ profile = None ,
373+ )
374+
375+ # Run the package context
376+ package_command_context .run ()
377+
378+ ecr_uploader = package_command_context .uploaders .ecr
379+
380+ # Access docker_client multiple times
381+ docker_client1 = ecr_uploader .docker_client
382+ docker_client2 = ecr_uploader .docker_client
383+ docker_client3 = ecr_uploader .docker_client
384+
385+ # Docker client validation should only be called once
386+ mock_get_validated_client .assert_called_once ()
387+ self .assertEqual (docker_client1 , docker_client2 )
388+ self .assertEqual (docker_client2 , docker_client3 )
389+ self .assertEqual (docker_client1 , mock_docker_client )
0 commit comments