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

Add Include resource functionality to npm and npm/esbuild #431

Closed
wants to merge 14 commits into from

Conversation

jasonterando
Copy link

Issue #: 430

Description of changes:

  • Addition of support for Metadata Include property that can be set to a file mask, or list of file masks, of files in the source directory, and copy those to the artifact (output) directory after build/packaging
  • An action called CopyResourceAction and utilities glob_copy and get_option_from_args have been added to support the functionality

Note: esbuild will work with the SAM CLI as-is, but for npm (without esbuild), SAM CLI will need to be updated to support sending include (currently, it will only send use_npm_ci and no other Metadata options).

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@jasonterando jasonterando requested a review from a team as a code owner January 25, 2023 22:22
@github-actions github-actions bot added area/workflow/node_npm pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels Jan 25, 2023
@qingchm
Copy link
Contributor

qingchm commented Jan 30, 2023

Thanks for opening this pull request! We will look at your feature request and see if your changes can be accepted!

@torresxb1 torresxb1 removed the stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. label Jan 31, 2023
@jasonterando
Copy link
Author

I'll take a look and see what is causing unit tests to fail in Windows

Copy link
Contributor

@mildaniel mildaniel left a comment

Choose a reason for hiding this comment

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

Thanks for taking the time to raise this PR! I've left a few comments but overall it looks good. I do wonder if this should be applied to other runtimes since it seems it's applicable to more than just Node.js (maybe compiled languages).

Would you also be able to raise the corresponding SAM-CLI PR for sending this parameter down?

known_paths = []
for item in items:
if Path(item).is_absolute():
raise ValueError('"{item}" is not a relative path'.format(item=item))
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use custom exceptions instead of raising ValueError.

Copy link
Author

@jasonterando jasonterando Feb 1, 2023

Choose a reason for hiding this comment

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

Any specific one you want me to use, or should I roll one? Would something like this work?

class FileOperationError(LambdaBuilderError):
    """
    Raised when a file operation fails
    """

    MESSAGE = "{operation_name} - {reason}"

def get_option_from_args(args: Union[None, Dict[str, any]], option_name: str) -> any:
if args is not None:
if "options" in args:
if args["options"] is not None:
Copy link
Contributor

@mildaniel mildaniel Feb 1, 2023

Choose a reason for hiding this comment

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

Let's do

if args.get("options") is not None: 

instead so that if options isn't there, it doesn't raise a key error. And the same thing for wherever else we do dictionary accesses like this

Copy link
Author

Choose a reason for hiding this comment

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

Roger that. Is this the correct way - should I be doing something more "pythonic"?

def get_option_from_args(args: Union[None, Dict[str, any]], option_name: str) -> any:
    if args is not None:
        options = args.get("options", None)
        if options is not None:
            return options.get(option_name, None)
    return None

@jasonterando
Copy link
Author

Would you also be able to raise the corresponding SAM-CLI PR for sending this parameter down?

Assuming we can get this shepherded through, sure!

@jasonterando
Copy link
Author

I've updated the following:

  1. Created a FileOperationError to raise when there is a problem copying
  2. Refactored get_option_from_args to implement the suggested .get syntax
  3. Added function robust_rmtree to facilitate retries for shutil.rmtree to workaround a Python on Windows issue (Python issue 40143)

Copy link
Contributor

@mildaniel mildaniel left a comment

Choose a reason for hiding this comment

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

Another thing to consider, and I'll take ownership of this, is that this will require a documentation update to publicize this new option.

Raised when a file operation fails
"""

MESSAGE = "{operation_name} - {reason}"
Copy link
Contributor

Choose a reason for hiding this comment

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

Make sure to run make black to reformat according to our style guide.

Copy link
Author

Choose a reason for hiding this comment

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

Sorry, missed that

@@ -222,3 +225,53 @@ def extract_tarfile(tarfile_path: Union[str, os.PathLike], unpack_dir: Union[str
raise tarfile.ExtractError("Attempted Path Traversal in Tar File")

tar.extractall(unpack_dir)


def glob_copy(source: Union[str, list], destination: str) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add type hints and docstrings to all functions.

Copy link
Author

Choose a reason for hiding this comment

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

Sure thing

dest_path = Path(destination)
known_paths = []
for item in items:
if os.path.isabs(item.replace('\\', '/')):
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need the item.replace()? I think os.path.isabs() should already handle this, I could be wrong though.

Copy link
Author

Choose a reason for hiding this comment

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

I had to have the replace in there to get the "absolute" detection to work properly under Windows. "\foo" is considered a relative path, but "/foo" is absolute (go figure). The purposes of this call is to make sure that the requested asset 's path is relative to the project, so either "\foo" or "/foo" should be treated as non-relative.

if isinstance(include, (list, str)):
self.actions.append(CopyResourceAction(source_dir, include, artifacts_dir))
elif include is not None:
raise ValueError("Resource include items must be strings or lists of strings")
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use a custom exception here too.

Copy link
Author

Choose a reason for hiding this comment

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

Yes sir

shutil.copyfile(file, save_to)


def robust_rmtree(path, timeout=1):
Copy link
Contributor

Choose a reason for hiding this comment

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

If this is only used in tests, we should move it there.

Copy link
Author

Choose a reason for hiding this comment

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

Moved to test/functional/test_utils.py

@jasonterando
Copy link
Author

If I take a stab at working on this stuff over the weekend, can you enable me to have Build & Test checks run. I promise not to submit 100 times.

@mildaniel
Copy link
Contributor

If I take a stab at working on this stuff over the weekend, can you enable me to have Build & Test checks run. I promise not to submit 100 times.

Already enabled 👍

@jasonterando
Copy link
Author

Hi, latest changes committed - make pr passed and I got all but one Gradle integration test to run (is there a Dockerfile available to set up Java, Go, Ruby, .NET, NodeJS?).

With regard to documentation, happy to submit it if you tell me where it should go and in what format. Here's some Markdown I've put together as a first pass.

## **Include** Resources (optional)

This optional build property for nodejs_npm and nodejs_npm_esbuild workflows will copy files to the output (artifacts) directory, after bundling, for inclusion in deployment.

Valid value types include string, dictionary, or a list containing either strings and/or dictionaries.

### String Parameter

Use this to deep-copy files matching the specfied glob-style parameter to the artifact directory.

Example:

```yaml
Resources:
  MyLambda:
    Type: AWS::Serverless::Function
    Metadata:
       BuildMethod: esbuild
       BulidProperties:
         Include: resources/*.json
```

*Project (Source) Directory*
```
index.ts
  /resources
    file1.json
    file2.json
      /data
        file3.json
```

*Artifact (Destination) Directory*

```
index.js
file1.json
file2.json
/data
  file3.json
```

### Dictionary (Object) Parameter

Use this to control where files are copied to in the artifact directory and whether they are deep-copied.

Example:

```yaml
Resources:
  MyLambda:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Include:
          Source: resources/*.json
          Destination: resources
          Recursive: False
```

*Project (Source) Directory*
```
index.ts
  /resources
    file1.json
    file2.json
      /data
        file3.json
```

*Artifact (Destination) Directory*

```
index.js
  /resources
    file1.json
    file2.json
```

## List Parameter

A list of strings and dictionaries as described above can be used to trigger multiple copy operations.

Copy link
Contributor

@moelasmar moelasmar left a comment

Choose a reason for hiding this comment

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

I have a general comment on this PR, I feel that this action is not related to a specific Runtime, and instead it can be part of sam build command or may be sam package but not part of aws-lambda-builders library. I will raise this PR with the team to discuss what is the better approach to implement this feature.

I will make this PR as draft till we discuss it, and will update it later.

@moelasmar moelasmar marked this pull request as draft May 25, 2023 05:52
@jasonterando
Copy link
Author

I have a general comment on this PR, I feel that this action is not related to a specific Runtime, and instead it can be part of sam build command or may be sam package but not part of aws-lambda-builders library. I will raise this PR with the team to discuss what is the better approach to implement this feature.

I will make this PR as draft till we discuss it, and will update it later.

I don't disagree that, ideally, this would be a Runtime-independent feature. However, even within NodeJS, there are differences in when you implement it, based upon whether esbuild is being used or not. You could go the route of creating some kind of event-driven mechanism so that the individual Runtime workflows can take actions (before build, after build, copy resources after build, etc.), but that isn't there today.

Anyways, it's months later and I had to end up going a different route (orchestrating esbuild external to SAM). If you want to nuke this PR, that's fine, we have a workaround, although it would have been nicer to use the integrated esbuild workflow.

@mildaniel
Copy link
Contributor

Closing until we figure out the correct path forward. Consolidating include/exclude functionality design and discussion into a single issue #516.

@mildaniel mildaniel closed this Jun 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants