Skip to content

Commit 6eb0a1b

Browse files
Merge from aws/aws-sam-cli/develop
2 parents 7962c4e + 0ef8319 commit 6eb0a1b

File tree

5 files changed

+273
-9
lines changed

5 files changed

+273
-9
lines changed

samcli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
SAM CLI version
33
"""
44

5-
__version__ = "1.145.0"
5+
__version__ = "1.145.1"

samcli/commands/package/package_context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
from samcli.lib.utils.boto_utils import get_boto_config_with_user_agent
3535
from samcli.lib.utils.preview_runtimes import PREVIEW_RUNTIMES
3636
from samcli.lib.utils.resources import AWS_LAMBDA_FUNCTION, AWS_SERVERLESS_FUNCTION
37-
from samcli.local.docker.utils import get_validated_container_client
3837
from samcli.yamlhelper import yaml_dump
3938

4039
LOG = logging.getLogger(__name__)
@@ -123,7 +122,8 @@ def run(self):
123122
)
124123
ecr_client = boto3.client("ecr", config=get_boto_config_with_user_agent(region_name=region_name))
125124

126-
docker_client = get_validated_container_client()
125+
# Pass None instead of validating Docker client upfront - ECRUploader will validate only when needed
126+
docker_client = None
127127

128128
s3_uploader = S3Uploader(
129129
s3_client, self.s3_bucket, self.s3_prefix, self.kms_key_id, self.force_upload, self.no_progressbar

samcli/lib/package/ecr_uploader.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ class ECRUploader:
3838
def __init__(
3939
self, docker_client, ecr_client, ecr_repo, ecr_repo_multi, no_progressbar=False, tag="latest", stream=stderr()
4040
):
41-
self.docker_client = docker_client if docker_client else get_validated_container_client()
41+
# Allow None docker_client, validate only when needed
42+
self._docker_client_param = docker_client
43+
self._validated_docker_client = None
4244
self.ecr_client = ecr_client
4345
self.ecr_repo = ecr_repo
4446
self.ecr_repo_multi = ecr_repo_multi
@@ -49,6 +51,17 @@ def __init__(
4951
self.log_streamer = LogStreamer(stream=self.stream)
5052
self.login_session_active = False
5153

54+
@property
55+
def docker_client(self):
56+
"""Lazy initialization of Docker client - only validates when ECR operations are needed."""
57+
if self._validated_docker_client is None:
58+
if self._docker_client_param is None:
59+
# Only validate Docker client when ECR operations are actually needed
60+
self._validated_docker_client = get_validated_container_client()
61+
else:
62+
self._validated_docker_client = self._docker_client_param
63+
return self._validated_docker_client
64+
5265
def login(self):
5366
"""
5467
Logs into the supplied ECR with credentials.

tests/unit/commands/package/test_package_context.py

Lines changed: 178 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

tests/unit/lib/package/test_ecr_uploader.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,84 @@ def test_ecr_uploader_init(self):
4949
tag=self.tag,
5050
)
5151

52+
def test_ecr_uploader_init_with_none_docker_client(self):
53+
"""Test ECRUploader initialization with None docker_client (lazy initialization)"""
54+
ecr_uploader = ECRUploader(
55+
docker_client=None, # Pass None to trigger lazy initialization
56+
ecr_client=self.ecr_client,
57+
ecr_repo=self.ecr_repo,
58+
ecr_repo_multi=self.ecr_repo_multi,
59+
tag=self.tag,
60+
)
61+
62+
# Docker client should not be validated yet
63+
self.assertIsNone(ecr_uploader._docker_client_param)
64+
self.assertIsNone(ecr_uploader._validated_docker_client)
65+
66+
@patch("samcli.lib.package.ecr_uploader.get_validated_container_client")
67+
def test_lazy_docker_client_validation_on_first_access(self, mock_get_validated_client):
68+
"""Test that Docker client is only validated when first accessed"""
69+
mock_docker_client = MagicMock()
70+
mock_get_validated_client.return_value = mock_docker_client
71+
72+
ecr_uploader = ECRUploader(
73+
docker_client=None, # Trigger lazy initialization
74+
ecr_client=self.ecr_client,
75+
ecr_repo=self.ecr_repo,
76+
ecr_repo_multi=self.ecr_repo_multi,
77+
tag=self.tag,
78+
)
79+
80+
# Docker client should not be validated yet
81+
self.assertIsNone(ecr_uploader._validated_docker_client)
82+
mock_get_validated_client.assert_not_called()
83+
84+
# First access should trigger validation
85+
docker_client = ecr_uploader.docker_client
86+
87+
# Validation should have been called
88+
mock_get_validated_client.assert_called_once()
89+
self.assertEqual(docker_client, mock_docker_client)
90+
self.assertEqual(ecr_uploader._validated_docker_client, mock_docker_client)
91+
92+
@patch("samcli.lib.package.ecr_uploader.get_validated_container_client")
93+
def test_lazy_docker_client_validation_cached_after_first_access(self, mock_get_validated_client):
94+
"""Test that Docker client validation is cached after first access"""
95+
mock_docker_client = MagicMock()
96+
mock_get_validated_client.return_value = mock_docker_client
97+
98+
ecr_uploader = ECRUploader(
99+
docker_client=None,
100+
ecr_client=self.ecr_client,
101+
ecr_repo=self.ecr_repo,
102+
ecr_repo_multi=self.ecr_repo_multi,
103+
tag=self.tag,
104+
)
105+
106+
# First access
107+
docker_client1 = ecr_uploader.docker_client
108+
# Second access
109+
docker_client2 = ecr_uploader.docker_client
110+
111+
# Validation should only be called once
112+
mock_get_validated_client.assert_called_once()
113+
self.assertEqual(docker_client1, docker_client2)
114+
self.assertEqual(docker_client1, mock_docker_client)
115+
116+
def test_docker_client_property_with_provided_client(self):
117+
"""Test that provided Docker client is used without validation"""
118+
ecr_uploader = ECRUploader(
119+
docker_client=self.docker_client, # Provide actual client
120+
ecr_client=self.ecr_client,
121+
ecr_repo=self.ecr_repo,
122+
ecr_repo_multi=self.ecr_repo_multi,
123+
tag=self.tag,
124+
)
125+
126+
# Should return the provided client
127+
docker_client = ecr_uploader.docker_client
128+
self.assertEqual(docker_client, self.docker_client)
129+
52130
self.assertEqual(ecr_uploader.docker_client, self.docker_client)
53131
self.assertEqual(ecr_uploader.ecr_repo, self.ecr_repo)
54132
self.assertEqual(ecr_uploader.tag, self.tag)

0 commit comments

Comments
 (0)