Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support dotnet lambda container builds #4665

Merged
merged 37 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d93a5ba
allow use container for dotnet with write permission to source code d…
mrkdeng Dec 14, 2022
31b7083
Merge branch 'develop' into develop
mrkdeng Dec 14, 2022
986a804
fix typo
mrkdeng Dec 14, 2022
654b0dc
Merge branch 'aws:develop' into develop
mrkdeng Jan 24, 2023
59e5ff6
Merge branch 'aws:develop' into mount_with_write
mrkdeng Jan 27, 2023
c7c4149
major code changes to allow mount with write
mrkdeng Feb 1, 2023
84c623b
remove dotnet env vars and add logic to make tmp dir on host
mrkdeng Feb 6, 2023
3a48b99
integration tests
mrkdeng Feb 7, 2023
871e9d2
consist help text and click confirm
mrkdeng Feb 7, 2023
628da19
cleaner approach to make tmp dir on host, refactor regarding dev guide
mrkdeng Feb 9, 2023
1c30b3f
update and add new unit tests
mrkdeng Feb 9, 2023
f7a013d
fix failed unit tests
mrkdeng Feb 13, 2023
ef34e17
Merge branch 'aws:develop' into develop
mrkdeng Feb 13, 2023
f03cd46
Merge branch 'develop' into mount_with_write
mrkdeng Feb 13, 2023
89b2414
update test comments
mrkdeng Feb 13, 2023
99d785e
update CONFIG and remove unnecessary Json dump
mrkdeng Feb 14, 2023
b9432ea
update integration tests
mrkdeng Feb 17, 2023
f3716f8
Merge branch 'develop' into mount_with_write
mrkdeng Feb 17, 2023
05ba6cc
Merge branch 'mount_with_write' of https://github.com/mrkdeng/aws-sam…
mrkdeng Feb 17, 2023
6819eed
Merge branch 'develop' into mount_with_write
mrkdeng Feb 20, 2023
ccde63c
pass ruff tests
mrkdeng Feb 20, 2023
60ceabf
Merge branch 'develop' into mount_with_write
mrkdeng Feb 22, 2023
888aadf
change mount with click to enum
mrkdeng Feb 23, 2023
db30045
add debug logging and remove unused exception
mrkdeng Feb 23, 2023
461202d
update unit and integration tests
mrkdeng Feb 23, 2023
ca2e77b
update click
mrkdeng Feb 23, 2023
6c7217e
add MountMode enum and set default to READ
mrkdeng Feb 24, 2023
8a22584
add unit tests for prompting
mrkdeng Feb 24, 2023
4196279
default mount_with to READ in all places
mrkdeng Feb 24, 2023
ccfb879
early exit
mrkdeng Feb 28, 2023
a2f15e6
better use of enum and constant, update unit tests
mrkdeng Mar 2, 2023
d00e842
Merge branch 'develop' into mount_with_write
mrkdeng Mar 3, 2023
6a2afca
Merge branch 'mount_with_write' of https://github.com/mrkdeng/aws-sam…
mrkdeng Mar 3, 2023
280b160
make tmp dir on sam build dir on host
mrkdeng Mar 6, 2023
2ba803b
Merge branch 'develop' into mount_with_write
mndeveci Mar 7, 2023
7f5d6a4
Merge branch 'develop' into mount_with_write
mndeveci Mar 10, 2023
bc63a8a
Merge branch 'develop' into mount_with_write
mndeveci Mar 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions samcli/commands/build/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import click

from samcli.commands.build.utils import prompt_user_to_enable_mount_with_write_if_needed, MountMode
from samcli.lib.build.bundler import EsbuildBundlerManager
from samcli.lib.providers.sam_api_provider import SamApiProvider
from samcli.lib.telemetry.event import EventTracker
Expand All @@ -35,7 +36,6 @@
ApplicationBuilder,
BuildError,
UnsupportedBuilderLibraryVersionError,
ContainerBuildNotSupported,
ApplicationBuildResult,
)
from samcli.commands._utils.constants import DEFAULT_BUILD_DIR
Expand Down Expand Up @@ -78,6 +78,7 @@ def __init__(
locate_layer_nested: bool = False,
hook_name: Optional[str] = None,
build_in_source: Optional[bool] = None,
mount_with=MountMode.READ.value,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mount_with=MountMode.READ.value,
mount_with: MountMode = MountMode.READ

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Click.Choice takes [str] only. Workaround: Convert str to enum in BuildContext.init()

self._mount_with = MountMode(mount_with)

) -> None:
"""
Initialize the class
Expand Down Expand Up @@ -133,6 +134,8 @@ def __init__(
Name of the hook package
build_in_source: Optional[bool]
Set to True to build in the source directory.
mount_with:
Mount mode of source code directory when building inside container, READ ONLY by default
"""

self._resource_identifier = resource_identifier
Expand Down Expand Up @@ -172,6 +175,7 @@ def __init__(
self._locate_layer_nested = locate_layer_nested
self._hook_name = hook_name
self._build_in_source = build_in_source
self._mount_with = mount_with

def __enter__(self) -> "BuildContext":
self.set_up()
Expand Down Expand Up @@ -235,6 +239,19 @@ def run(self):

self._stacks = self._handle_build_pre_processing()

# boolean value indicates if mount with write or not, defaults to READ ONLY
mount_with_write = False
if self._use_container:
if self._mount_with.lower() == MountMode.WRITE.value.lower():
mrkdeng marked this conversation as resolved.
Show resolved Hide resolved
mount_with_write = True
else:
# if self._mount_with is NOT WRITE
# check the need of mounting with write permissions and prompt user to enable it if needed
mount_with_write = prompt_user_to_enable_mount_with_write_if_needed(
self.get_resources_to_build(),
self.base_dir,
)

try:
builder = ApplicationBuilder(
self.get_resources_to_build(),
Expand All @@ -252,6 +269,7 @@ def run(self):
build_images=self._build_images,
combine_dependencies=not self._create_auto_dependency_layer,
build_in_source=self._build_in_source,
mount_with_write=mount_with_write,
)
except FunctionNotFound as ex:
raise UserException(str(ex), wrapped_from=ex.__class__.__name__) from ex
Expand Down Expand Up @@ -296,7 +314,6 @@ def run(self):
BuildError,
BuildInsideContainerError,
UnsupportedBuilderLibraryVersionError,
ContainerBuildNotSupported,
mrkdeng marked this conversation as resolved.
Show resolved Hide resolved
InvalidBuildGraphException,
) as ex:
click.secho("\nBuild Failed", fg="red")
Expand Down
15 changes: 15 additions & 0 deletions samcli/commands/build/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from samcli.cli.cli_config_file import configuration_option, TomlProvider
from samcli.lib.utils.version_checker import check_newer_version
from samcli.commands.build.click_container import ContainerOptions
from samcli.commands.build.utils import MountMode

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -136,6 +137,16 @@
help="Enabled parallel builds. Use this flag to build your AWS SAM template's functions and layers in parallel. "
"By default the functions and layers are built in sequence",
)
@click.option(
"--mount-with",
"-mw",
type=click.Choice(MountMode.values(), case_sensitive=False),
mrkdeng marked this conversation as resolved.
Show resolved Hide resolved
default=MountMode.READ.value,
help="Optional. Specify mount mode for building functions/layers inside container. "
"If it is mounted with write permissions, some files in source code directory may "
"be changed/added by the build process. By default the source code directory is read only.",
cls=ContainerOptions,
)
@build_dir_option
@cache_dir_option
@base_dir_option
Expand Down Expand Up @@ -175,6 +186,7 @@ def cli(
config_env: str,
hook_name: Optional[str],
skip_prepare_infra: bool,
mount_with=MountMode.READ.value,
mrkdeng marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""
`sam build` command entry point
Expand Down Expand Up @@ -205,6 +217,7 @@ def cli(
exclude,
hook_name,
None, # TODO: replace with build_in_source once it's added as a click option
mount_with,
) # pragma: no cover


Expand All @@ -230,6 +243,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
exclude: Optional[Tuple[str, ...]],
hook_name: Optional[str],
build_in_source: Optional[bool],
mount_with=MountMode.READ.value,
mrkdeng marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""
Implementation of the ``cli`` method
Expand Down Expand Up @@ -275,6 +289,7 @@ def do_cli( # pylint: disable=too-many-locals, too-many-statements
aws_region=click_ctx.region,
hook_name=hook_name,
build_in_source=build_in_source,
mount_with=mount_with,
mndeveci marked this conversation as resolved.
Show resolved Hide resolved
) as ctx:
ctx.run()

Expand Down
110 changes: 110 additions & 0 deletions samcli/commands/build/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Utilities for sam build command
"""
import pathlib
from enum import Enum
from typing import List

import click

from samcli.lib.build.workflow_config import CONFIG, get_workflow_config
from samcli.lib.providers.provider import ResourcesToBuildCollector


class MountMode(Enum):
"""
Enums that represent mount mode used when build lambda functions/layers inside container
"""

READ = "READ"
WRITE = "WRITE"

@classmethod
def values(cls) -> List[str]:
"""
A getter to retrieve the accepted value list for mount mode

Returns: List[str]
The accepted mount mode list
"""
return [e.value for e in cls]
mrkdeng marked this conversation as resolved.
Show resolved Hide resolved


def prompt_user_to_enable_mount_with_write_if_needed(
resources_to_build: ResourcesToBuildCollector,
base_dir: str,
) -> bool:
"""
First check if mounting with write permissions is needed for building inside container or not. If it is needed, then
prompt user to choose if enables mounting with write permissions or not.

Parameters
----------
resources_to_build:
Resource to build inside container

base_dir : str
Path to the base directory

Returns
-------
bool
True, if user enabled mounting with write permissions.
"""

for function in resources_to_build.functions:
code_uri = function.codeuri
if not code_uri:
continue
runtime = function.runtime
code_dir = str(pathlib.Path(base_dir, code_uri).resolve())
# get specified_workflow if metadata exists
metadata = function.metadata
specified_workflow = metadata.get("BuildMethod", None) if metadata else None
config = get_workflow_config(runtime, code_dir, base_dir, specified_workflow)
# at least one function needs mount with write, return with prompting
if not config.must_mount_with_write_in_container:
continue
return prompt(config, code_dir)

for layer in resources_to_build.layers:
code_uri = layer.codeuri
if not code_uri:
continue
code_dir = str(pathlib.Path(base_dir, code_uri).resolve())
specified_workflow = layer.build_method
config = get_workflow_config(None, code_dir, base_dir, specified_workflow)
# at least one layer needs mount with write, return with prompting
if not config.must_mount_with_write_in_container:
continue
return prompt(config, code_dir)

return False


def prompt(config: CONFIG, source_dir: str) -> bool:
"""
Prompt user to choose if enables mounting with write permissions or not when building lambda functions/layers

Parameters
----------
config: namedtuple(Capability)
Config specifying the particular build workflow

source_dir : str
Path to the function source code

Returns
-------
bool
True, if user enabled mounting with write permissions.
"""
if click.confirm(
f"\nBuilding functions with {config.language} inside containers needs "
f"mounting with write permissions to the source code directory {source_dir}. "
f"Some files in this directory may be changed or added by the build process. "
f"Pass `--mount-with WRITE` to `sam build` CLI to avoid this confirmation. "
f"\nWould you like to enable mounting with write permissions? "
):
return True
return False
31 changes: 16 additions & 15 deletions samcli/lib/build/app_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@
import logging
import pathlib
from typing import List, Optional, Dict, cast, NamedTuple

import docker
import docker.errors
from aws_lambda_builders import (
RPC_PROTOCOL_VERSION as lambda_builders_protocol_version,
)
from aws_lambda_builders.builder import LambdaBuilder
from aws_lambda_builders.exceptions import LambdaBuilderError

from samcli.lib.build.build_graph import FunctionBuildDefinition, LayerBuildDefinition, BuildGraph
from samcli.lib.build.build_strategy import (
DefaultBuildStrategy,
Expand Down Expand Up @@ -51,14 +49,12 @@
DockerBuildFailed,
BuildError,
BuildInsideContainerError,
ContainerBuildNotSupported,
UnsupportedBuilderLibraryVersionError,
)

from samcli.lib.build.workflow_config import (
get_workflow_config,
supports_specified_workflow,
get_layer_subfolder,
supports_build_in_container,
CONFIG,
UnsupportedRuntimeException,
)
Expand Down Expand Up @@ -101,6 +97,7 @@ def __init__(
build_images: Optional[Dict] = None,
combine_dependencies: bool = True,
build_in_source: Optional[bool] = None,
mount_with_write: bool = False,
) -> None:
"""
Initialize the class
Expand Down Expand Up @@ -144,6 +141,8 @@ def __init__(
dependencies or not.
build_in_source: Optional[bool]
Set to True to build in the source directory.
mount_with_write: bool
Mount source code directory with write permissions when building inside container.
"""
self._resources_to_build = resources_to_build
self._build_dir = build_dir
Expand All @@ -166,6 +165,7 @@ def __init__(
self._build_images = build_images or {}
self._combine_dependencies = combine_dependencies
self._build_in_source = build_in_source
self._mount_with_write = mount_with_write

def build(self) -> ApplicationBuildResult:
"""
Expand Down Expand Up @@ -544,6 +544,8 @@ def _build_layer(
build_runtime = compatible_runtimes[0]
global_image = self._build_images.get(None)
image = self._build_images.get(layer_name, global_image)
# pass to container only when specified workflow is supported to overwrite runtime to get image
supported_specified_workflow = supports_specified_workflow(specified_workflow)
self._build_function_on_container(
config,
code_dir,
Expand All @@ -555,6 +557,7 @@ def _build_layer(
container_env_vars,
image,
is_building_layer=True,
specified_workflow=specified_workflow if supported_specified_workflow else None,
)
else:
self._build_function_in_process(
Expand Down Expand Up @@ -644,11 +647,9 @@ def _build_function( # pylint: disable=R1710
# Create the arguments to pass to the builder
# Code is always relative to the given base directory.
code_dir = str(pathlib.Path(self._base_dir, codeuri).resolve())

# Determine if there was a build workflow that was specified directly in the template.
specified_build_workflow = metadata.get("BuildMethod", None) if metadata else None

config = get_workflow_config(runtime, code_dir, self._base_dir, specified_workflow=specified_build_workflow)
specified_workflow = metadata.get("BuildMethod", None) if metadata else None
config = get_workflow_config(runtime, code_dir, self._base_dir, specified_workflow=specified_workflow)

if config.language == "provided" and isinstance(metadata, dict) and metadata.get("ProjectRootDirectory"):
code_dir = str(pathlib.Path(self._base_dir, metadata.get("ProjectRootDirectory", code_dir)).resolve())
Expand Down Expand Up @@ -682,7 +683,8 @@ def _build_function( # pylint: disable=R1710
# None represents the global build image for all functions/layers
global_image = self._build_images.get(None)
image = self._build_images.get(function_name, global_image)

# pass to container only when specified workflow is supported to overwrite runtime to get image
supported_specified_workflow = supports_specified_workflow(specified_workflow)
return self._build_function_on_container(
config,
code_dir,
Expand All @@ -693,6 +695,7 @@ def _build_function( # pylint: disable=R1710
options,
container_env_vars,
image,
specified_workflow=specified_workflow if supported_specified_workflow else None,
)

return self._build_function_in_process(
Expand Down Expand Up @@ -884,6 +887,7 @@ def _build_function_on_container(
container_env_vars: Optional[Dict] = None,
build_image: Optional[str] = None,
is_building_layer: bool = False,
specified_workflow: Optional[str] = None,
) -> str:
# _build_function_on_container() is only called when self._container_manager if not None
if not self._container_manager:
Expand All @@ -894,10 +898,6 @@ def _build_function_on_container(
"Docker is unreachable. Docker needs to be running to build inside a container."
)

container_build_supported, reason = supports_build_in_container(config)
if not container_build_supported:
raise ContainerBuildNotSupported(reason)

# If we are printing debug logs in SAM CLI, the builder library should also print debug logs
log_level = LOG.getEffectiveLevel()

Expand All @@ -912,6 +912,7 @@ def _build_function_on_container(
manifest_path,
runtime,
architecture,
specified_workflow=specified_workflow,
log_level=log_level,
optimizations=None,
options=options,
Expand All @@ -921,6 +922,7 @@ def _build_function_on_container(
image=build_image,
is_building_layer=is_building_layer,
build_in_source=self._build_in_source,
mount_with_write=self._mount_with_write,
)

try:
Expand All @@ -931,7 +933,6 @@ def _build_function_on_container(
raise UnsupportedBuilderLibraryVersionError(
container.image, "{} executable not found in container".format(container.executable_name)
) from ex

# Container's output provides status of whether the build succeeded or failed
# stdout contains the result of JSON-RPC call
stdout_stream = io.BytesIO()
Expand Down
Loading