Skip to content

Commit 9824761

Browse files
feat: add ability to pass docker build params in template (aws#8913)
* feat: add ability to pass docker build params in template * test: add unit tests for docker build extra params * fix: typecheck DockerExtraBuildParams * nit: add extra validation --------- Co-authored-by: Roger Zhang <ruojiazh@amazon.com>
1 parent 5a39057 commit 9824761

6 files changed

Lines changed: 104 additions & 0 deletions

File tree

samcli/lib/build/app_builder.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,13 +434,20 @@ def _build_lambda_image(self, function_name: str, metadata: Dict, architecture:
434434
docker_tag = f"{image_name.lower()}:{tag}"
435435
docker_build_target = metadata.get("DockerBuildTarget", None)
436436
docker_build_args = metadata.get("DockerBuildArgs", {})
437+
docker_build_extra_params = metadata.get("DockerBuildExtraParams", None)
437438

438439
if not dockerfile or not docker_context:
439440
raise DockerBuildFailed("Docker file or Docker context metadata are missed.")
440441

441442
if not isinstance(docker_build_args, dict):
442443
raise DockerBuildFailed("DockerBuildArgs needs to be a dictionary!")
443444

445+
if docker_build_extra_params is not None and (
446+
not isinstance(docker_build_extra_params, list)
447+
or not all(isinstance(p, str) for p in docker_build_extra_params)
448+
):
449+
raise DockerBuildFailed("DockerBuildExtraParams must be a list of strings!")
450+
444451
docker_context_dir = pathlib.Path(self._base_dir, docker_context).resolve()
445452

446453
if os.environ.get("SAM_BUILD_MODE") and isinstance(docker_build_args, dict):
@@ -460,6 +467,8 @@ def _build_lambda_image(self, function_name: str, metadata: Dict, architecture:
460467
}
461468
if docker_build_target:
462469
build_args["target"] = cast(str, docker_build_target)
470+
if docker_build_extra_params:
471+
build_args["extra_params"] = docker_build_extra_params
463472

464473
try:
465474
if not self._image_build_client:

samcli/lib/samlib/resource_metadata_normalizer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
IMAGE_ASSET_PROPERTY = "Code.ImageUri"
2626
ASSET_DOCKERFILE_PATH_KEY = "aws:asset:dockerfile-path"
2727
ASSET_DOCKERFILE_BUILD_ARGS_KEY = "aws:asset:docker-build-args"
28+
ASSET_DOCKER_BUILD_EXTRA_PARAMS_KEY = "aws:asset:docker-build-extra-params"
2829

2930
SAM_RESOURCE_ID_KEY = "SamResourceId"
3031
SAM_IS_NORMALIZED = "SamNormalized"
3132
SAM_METADATA_DOCKERFILE_KEY = "Dockerfile"
3233
SAM_METADATA_DOCKER_CONTEXT_KEY = "DockerContext"
3334
SAM_METADATA_DOCKER_BUILD_ARGS_KEY = "DockerBuildArgs"
35+
SAM_METADATA_DOCKER_BUILD_EXTRA_PARAMS_KEY = "DockerBuildExtraParams"
3436

3537
ASSET_BUNDLED_METADATA_KEY = "aws:asset:is-bundled"
3638
SAM_METADATA_SKIP_BUILD_KEY = "SkipBuild"
@@ -189,6 +191,7 @@ def _extract_image_asset_metadata(metadata):
189191
SAM_METADATA_DOCKERFILE_KEY: str(dockerfile_path.as_posix()),
190192
SAM_METADATA_DOCKER_CONTEXT_KEY: str(asset_path),
191193
SAM_METADATA_DOCKER_BUILD_ARGS_KEY: metadata.get(ASSET_DOCKERFILE_BUILD_ARGS_KEY, {}),
194+
SAM_METADATA_DOCKER_BUILD_EXTRA_PARAMS_KEY: metadata.get(ASSET_DOCKER_BUILD_EXTRA_PARAMS_KEY, None),
192195
}
193196

194197
@staticmethod

samcli/local/docker/image_build_client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def build_image(
3939
platform: Optional[str] = None,
4040
target: Optional[str] = None,
4141
rm: bool = True,
42+
extra_params: Optional[list[str]] = None,
4243
) -> Generator[Dict[str, Any], None, None]:
4344
"""
4445
Build a container image from a Dockerfile.
@@ -59,6 +60,8 @@ def build_image(
5960
Build target stage in multi-stage Dockerfile
6061
rm : bool
6162
Remove intermediate containers after build (default: True)
63+
extra_params : list of str, optional
64+
Extra CLI flags (e.g., ["--ssh", "default"]). Only supported by CLIBuildClient
6265
6366
Yields
6467
------
@@ -119,6 +122,7 @@ def build_image(
119122
platform: Optional[str] = None,
120123
target: Optional[str] = None,
121124
rm: bool = True,
125+
extra_params: Optional[list[str]] = None,
122126
) -> Generator[Dict[str, Any], None, None]:
123127
"""Build image using docker-py SDK"""
124128
build_kwargs = {
@@ -135,6 +139,12 @@ def build_image(
135139
if target is not None:
136140
build_kwargs["target"] = target
137141

142+
if extra_params:
143+
LOG.warning(
144+
"DockerBuildExtraParams are not supported with the SDK build client and will be ignored. "
145+
"Use --use-buildkit to enable CLI-based builds."
146+
)
147+
138148
_, build_logs = self.container_client.images.build(**build_kwargs)
139149
return build_logs # type: ignore[no-any-return]
140150

@@ -159,6 +169,7 @@ def build_image(
159169
platform: Optional[str] = None,
160170
target: Optional[str] = None,
161171
rm: bool = True,
172+
extra_params: Optional[list[str]] = None,
162173
) -> Generator[Dict[str, Any], None, None]:
163174
# Make dockerfile path relative to context if not absolute
164175
if not os.path.isabs(dockerfile):
@@ -187,6 +198,9 @@ def build_image(
187198
if rm:
188199
cmd.append("--rm")
189200

201+
if extra_params:
202+
cmd.extend(extra_params)
203+
190204
cmd.append(path)
191205

192206
LOG.debug(f"Executing build command: {' '.join(cmd)}")

tests/unit/lib/build_module/test_app_builder.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,44 @@ def test_build_lambda_image_raises_for_non_dict_docker_build_args(self):
17741774
with self.assertRaises(DockerBuildFailed):
17751775
self.builder._build_lambda_image("Name", metadata, X86_64)
17761776

1777+
def test_can_build_image_function_with_extra_params(self):
1778+
metadata = {
1779+
"Dockerfile": "Dockerfile",
1780+
"DockerContext": "context",
1781+
"DockerTag": "Tag",
1782+
"DockerBuildArgs": {"a": "b"},
1783+
"DockerBuildExtraParams": ["--ssh", "default"],
1784+
}
1785+
1786+
mock_build_client = Mock()
1787+
mock_build_client.build_image.return_value = iter([])
1788+
self.builder._image_build_client = mock_build_client
1789+
1790+
self.builder._build_lambda_image("Name", metadata, X86_64)
1791+
1792+
call_kwargs = mock_build_client.build_image.call_args[1]
1793+
self.assertEqual(call_kwargs["extra_params"], ["--ssh", "default"])
1794+
1795+
def test_build_lambda_image_raises_for_non_list_docker_build_extra_params(self):
1796+
metadata = {
1797+
"Dockerfile": "Dockerfile",
1798+
"DockerContext": "context",
1799+
"DockerTag": "Tag",
1800+
"DockerBuildExtraParams": "--ssh default",
1801+
}
1802+
with self.assertRaises(DockerBuildFailed):
1803+
self.builder._build_lambda_image("Name", metadata, X86_64)
1804+
1805+
def test_build_lambda_image_raises_for_non_string_docker_build_extra_params_element(self):
1806+
metadata = {
1807+
"Dockerfile": "Dockerfile",
1808+
"DockerContext": "context",
1809+
"DockerTag": "Tag",
1810+
"DockerBuildExtraParams": ["--build-arg", 42],
1811+
}
1812+
with self.assertRaises(DockerBuildFailed):
1813+
self.builder._build_lambda_image("Name", metadata, X86_64)
1814+
17771815
def test_build_lambda_image_uses_latest_tag_when_not_specified(self):
17781816
metadata = {
17791817
"Dockerfile": "Dockerfile",

tests/unit/lib/samlib/test_resource_metadata_normalizer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def test_replace_all_resources_that_contain_metadata(self):
8080

8181
def test_replace_all_resources_that_contain_image_metadata(self):
8282
docker_build_args = {"arg1": "val1", "arg2": "val2"}
83+
docker_build_extra_params = ["--param1", "value1"]
8384
asset_path = pathlib.Path("/path", "to", "asset")
8485
dockerfile_path = pathlib.Path("path", "to", "Dockerfile")
8586
template_data = {
@@ -97,6 +98,7 @@ def test_replace_all_resources_that_contain_image_metadata(self):
9798
"aws:asset:property": "Code.ImageUri",
9899
"aws:asset:dockerfile-path": dockerfile_path,
99100
"aws:asset:docker-build-args": docker_build_args,
101+
"aws:asset:docker-build-extra-params": docker_build_extra_params,
100102
},
101103
},
102104
}
@@ -112,6 +114,9 @@ def test_replace_all_resources_that_contain_image_metadata(self):
112114
expected_dockerfile_path = str(pathlib.Path("path", "to", "Dockerfile").as_posix())
113115
self.assertEqual(expected_dockerfile_path, template_data["Resources"]["Function1"]["Metadata"]["Dockerfile"])
114116
self.assertEqual(docker_build_args, template_data["Resources"]["Function1"]["Metadata"]["DockerBuildArgs"])
117+
self.assertEqual(
118+
docker_build_extra_params, template_data["Resources"]["Function1"]["Metadata"]["DockerBuildExtraParams"]
119+
)
115120
self.assertEqual("Function1", template_data["Resources"]["Function1"]["Metadata"]["SamResourceId"])
116121

117122
def test_replace_all_resources_that_contain_image_metadata_dockerfile_extensions(self):

tests/unit/local/docker/test_image_build_client.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,19 @@ def test_build_image_minimal(self):
5656
rm=True,
5757
)
5858

59+
@patch("samcli.local.docker.image_build_client.LOG")
60+
def test_build_image_extra_params_logs_warning(self, mock_log):
61+
"""Test that extra_params triggers a warning and is not passed to SDK"""
62+
mock_image = Mock()
63+
mock_logs = iter([])
64+
65+
self.mock_container_client.images.build.return_value = (mock_image, mock_logs)
66+
self.client.build_image(**self.base_build_args, extra_params=["--ssh", "default"])
67+
mock_log.warning.assert_called_once()
68+
self.assertIn("DockerBuildExtraParams", mock_log.warning.call_args[0][0])
69+
70+
self.mock_container_client.images.build.assert_called_once_with(**self.base_build_args, rm=True)
71+
5972
def test_is_available_returns_true(self):
6073
"""Test that is_available always returns True for SDK"""
6174
result = SDKBuildClient.is_available("docker")
@@ -202,6 +215,28 @@ def test_build_image_handles_failure(self, mock_popen):
202215
self.assertIn("Build failed with exit code 1", str(context.exception))
203216
self.assertEqual(context.exception.build_log, [{"stream": "Step 1/5\n"}, {"stream": "Error: build failed\n"}])
204217

218+
@patch("samcli.local.docker.image_build_client.subprocess.Popen")
219+
def test_build_image_docker_with_extra_params(self, mock_popen):
220+
"""Test that extra_params are appended to the docker command"""
221+
mock_process = Mock()
222+
mock_process.stdout = iter(["Done\n"])
223+
mock_process.returncode = 0
224+
mock_popen.return_value = mock_process
225+
226+
list(
227+
self.docker_client.build_image(
228+
**self.base_build_args,
229+
extra_params=["--ssh", "default", "--secret", "id=mysecret,src=secret.txt"],
230+
)
231+
)
232+
233+
actual_cmd = mock_popen.call_args[0][0]
234+
self.assertEqual(actual_cmd[-1], self.base_build_args["path"])
235+
self.assertIn("--ssh", actual_cmd)
236+
self.assertIn("default", actual_cmd)
237+
self.assertIn("--secret", actual_cmd)
238+
self.assertIn("id=mysecret,src=secret.txt", actual_cmd)
239+
205240
@patch("samcli.local.docker.image_build_client.shutil.which")
206241
@patch("samcli.local.docker.image_build_client.subprocess.run")
207242
def test_is_available_docker_success(self, mock_run, mock_which):

0 commit comments

Comments
 (0)