Repository: aws/aws-sam-cli

Issue:
Bug: sam build omits Dockerfile suffix from build.toml when aws:asset:property == "Code.ImageUri"
### Description:

Hi all! I was unable to find an open or closed issue describing this exact problem.

The problem is very specific: when your SAM template specifies a Dockerfile containing a suffix in its name (e.g., `.production`) AND the `aws:asset:property` is set to `"Code.ImageUri"`, the Dockerfile suffix is not copied over to the generated `build.toml`, and thus SAM is unable to locate the Dockerfile. This occurs for both the `aws:asset:dockerfile-path` and `Dockerfile` properties in the template JSON.

This is important because this is the pathway by which CDK generates SAM templates which can be used for local testing and other deployment internals.

The problem boils down to the metadata normalization when `aws:asset:property` is `Code.ImageUri`. This leads to `_extract_image_asset_metadata` being called in which only the Dockerfile path's `stem` is extracted ([code permalink](https://github.com/aws/aws-sam-cli/blob/d634991615476a3980496694f90ac6c23800ab73/samcli/lib/samlib/resource_metadata_normalizer.py#L189)).

It seems to me that `dockerfile_path.name` should be used here instead of `dockerfile_path.stem` ([Python docs](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)). Is there some other rationale for using `.stem` here?

### Steps to reproduce:

1. Create a CDK project (e.g., [CDK hello world app](https://docs.aws.amazon.com/cdk/v2/guide/hello_world.html))
2. Add a simple Dockerfile **named `Dockerfile.production`**
3. Define a CDK Docker Lambda function. E.g.:

```typescript
new lambda.DockerImageFunction(
      this,
      "TestFunction",
      {
        code: lambda.DockerImageCode.fromImageAsset(
          path.join(__dirname, "..", "path", "to", "dockerfile"),
          {
            file: "Dockerfile.production",
          },
        ),
      },
    );
```

5. Change the name of your `Dockerfile` to `Dockerfile.prod`. Also update this reference in the template JSON/YAML.
6. `npx cdk synth --no-staging`
7. `sam build -t cdk.out/HelloCdk.template.json`
8. `sam local invoke`

### Observed result:

The build will "succeed" but you should not see a Docker build triggered. Open `.aws-sam/build.toml` and examine the function build definition metadata. You should see

```
"aws:asset:dockerfile-path" = "Dockerfile.production"
```

and

```
Dockerfile = "Dockerfile"
```

Upon attempting to invoke, you will see that SAM is unable to locate the Docker image to invoke the function (because it has not been built).

### Expected result:

The outputted `build.toml` should include:

```
Dockerfile = "Dockerfile.production"
```

`sam build` and `sam local invoke` should trigger a build of the Docker image and invoke the function by running that image, respectively.

### Additional environment details (Ex: Windows, Mac, Amazon Linux etc)

1. macOS 13.0
3. `sam --version`: SAM CLI, version 1.61.0
9. AWS region: local development



Discussion:
Thanks for creating this issue, it does look to be a bug. Would you be willing to raise a PR for it and we will prioritize on getting it through! cc: @moelasmar 
@sriram-mv thank you for the response! Yep absolutely, I can get a PR up sometime today.

Resolving patch:
diff --git a/samcli/lib/samlib/resource_metadata_normalizer.py b/samcli/lib/samlib/resource_metadata_normalizer.py
--- a/samcli/lib/samlib/resource_metadata_normalizer.py
+++ b/samcli/lib/samlib/resource_metadata_normalizer.py
@@ -186,7 +186,7 @@ def _extract_image_asset_metadata(metadata):
         """
         asset_path = Path(metadata.get(ASSET_PATH_METADATA_KEY, ""))
         dockerfile_path = Path(metadata.get(ASSET_DOCKERFILE_PATH_KEY), "")
-        dockerfile, path_from_asset = dockerfile_path.stem, dockerfile_path.parent
+        dockerfile, path_from_asset = dockerfile_path.name, dockerfile_path.parent
         dockerfile_context = str(Path(asset_path.joinpath(path_from_asset)))
         return {
             SAM_METADATA_DOCKERFILE_KEY: dockerfile,


Test patch:
diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py
--- a/tests/integration/buildcmd/test_build_cmd.py
+++ b/tests/integration/buildcmd/test_build_cmd.py
@@ -86,6 +86,34 @@ def test_with_default_requirements(self, runtime, use_container):
             self.built_template, self.FUNCTION_LOGICAL_ID_IMAGE, self._make_parameter_override_arg(overrides), expected
         )
 
+    @parameterized.expand([("3.6", False), ("3.7", False), ("3.8", False), ("3.9", False)])
+    @pytest.mark.flaky(reruns=3)
+    def test_with_dockerfile_extension(self, runtime, use_container):
+        _tag = f"{random.randint(1,100)}"
+        overrides = {
+            "Runtime": runtime,
+            "Handler": "main.handler",
+            "DockerFile": "Dockerfile.production",
+            "Tag": _tag,
+        }
+        cmdlist = self.get_command_list(use_container=use_container, parameter_overrides=overrides)
+
+        LOG.info("Running Command: ")
+        LOG.info(cmdlist)
+        run_command(cmdlist, cwd=self.working_dir)
+
+        self._verify_image_build_artifact(
+            self.built_template,
+            self.FUNCTION_LOGICAL_ID_IMAGE,
+            "ImageUri",
+            f"{self.FUNCTION_LOGICAL_ID_IMAGE.lower()}:{_tag}",
+        )
+
+        expected = {"pi": "3.14"}
+        self._verify_invoke_built_function(
+            self.built_template, self.FUNCTION_LOGICAL_ID_IMAGE, self._make_parameter_override_arg(overrides), expected
+        )
+
     @pytest.mark.flaky(reruns=3)
     def test_intermediate_container_deleted(self):
         _tag = f"{random.randint(1, 100)}"
diff --git a/tests/integration/testdata/buildcmd/PythonImage/Dockerfile.production b/tests/integration/testdata/buildcmd/PythonImage/Dockerfile.production
new file mode 100644
--- /dev/null
+++ b/tests/integration/testdata/buildcmd/PythonImage/Dockerfile.production
@@ -0,0 +1,15 @@
+ARG BASE_RUNTIME
+
+FROM public.ecr.aws/lambda/python:$BASE_RUNTIME
+
+ARG FUNCTION_DIR="/var/task"
+
+RUN mkdir -p $FUNCTION_DIR
+
+COPY main.py $FUNCTION_DIR
+
+COPY __init__.py $FUNCTION_DIR
+COPY requirements.txt $FUNCTION_DIR
+
+RUN python -m pip install -r $FUNCTION_DIR/requirements.txt -t $FUNCTION_DIR
+
diff --git a/tests/unit/lib/samlib/test_resource_metadata_normalizer.py b/tests/unit/lib/samlib/test_resource_metadata_normalizer.py
--- a/tests/unit/lib/samlib/test_resource_metadata_normalizer.py
+++ b/tests/unit/lib/samlib/test_resource_metadata_normalizer.py
@@ -112,6 +112,41 @@ def test_replace_all_resources_that_contain_image_metadata(self):
         self.assertEqual(docker_build_args, template_data["Resources"]["Function1"]["Metadata"]["DockerBuildArgs"])
         self.assertEqual("Function1", template_data["Resources"]["Function1"]["Metadata"]["SamResourceId"])
 
+    def test_replace_all_resources_that_contain_image_metadata_dockerfile_extensions(self):
+        docker_build_args = {"arg1": "val1", "arg2": "val2"}
+        asset_path = pathlib.Path("/path", "to", "asset")
+        dockerfile_path = pathlib.Path("path", "to", "Dockerfile.production")
+        template_data = {
+            "Resources": {
+                "Function1": {
+                    "Properties": {
+                        "Code": {
+                            "ImageUri": {
+                                "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:b5d75370ccc2882b90f701c8f98952aae957e85e1928adac8860222960209056"
+                            }
+                        }
+                    },
+                    "Metadata": {
+                        "aws:asset:path": asset_path,
+                        "aws:asset:property": "Code.ImageUri",
+                        "aws:asset:dockerfile-path": dockerfile_path,
+                        "aws:asset:docker-build-args": docker_build_args,
+                    },
+                },
+            }
+        }
+
+        ResourceMetadataNormalizer.normalize(template_data)
+
+        expected_docker_context_path = str(pathlib.Path("/path", "to", "asset", "path", "to"))
+        self.assertEqual("function1", template_data["Resources"]["Function1"]["Properties"]["Code"]["ImageUri"])
+        self.assertEqual(
+            expected_docker_context_path, template_data["Resources"]["Function1"]["Metadata"]["DockerContext"]
+        )
+        self.assertEqual("Dockerfile.production", template_data["Resources"]["Function1"]["Metadata"]["Dockerfile"])
+        self.assertEqual(docker_build_args, template_data["Resources"]["Function1"]["Metadata"]["DockerBuildArgs"])
+        self.assertEqual("Function1", template_data["Resources"]["Function1"]["Metadata"]["SamResourceId"])
+
     def test_replace_all_resources_that_contain_image_metadata_windows_paths(self):
         docker_build_args = {"arg1": "val1", "arg2": "val2"}
         asset_path = "C:\\path\\to\\asset"