Skip to content

Commit

Permalink
Merge from aws/aws-sam-cli/develop
Browse files Browse the repository at this point in the history
  • Loading branch information
aws-sam-cli-bot authored Sep 6, 2022
2 parents 831d546 + 79006c4 commit 1c9a2a5
Show file tree
Hide file tree
Showing 15 changed files with 360 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@ Read the [SAM Documentation Contribution Guide](https://github.com/awsdocs/aws-s
started.

### Join the SAM Community on Slack
[Join the SAM developers channel (#samdev)](https://join.slack.com/t/awsdevelopers/shared_invite/zt-idww18e8-Z1kXhI7GNuDewkweCF3YjA) on Slack to collaborate with fellow community members and the AWS SAM team.
[Join the SAM developers channel (#samdev)](https://join.slack.com/t/awsdevelopers/shared_invite/zt-yryddays-C9fkWrmguDv0h2EEDzCqvw) on Slack to collaborate with fellow community members and the AWS SAM team.
2 changes: 1 addition & 1 deletion appveyor-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ install:
# AppVeyor's apt-get cache might be outdated, and the package could potentially be 404.
- sh: "sudo apt-get update"

- sh: "gvm use go1.13"
- sh: "gvm use go1.15"
- sh: "echo $PATH"
- sh: "ls /usr/"
- sh: "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64"
Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ for:
# AppVeyor's apt-get cache might be outdated, and the package could potentially be 404.
- sh: "sudo apt-get update"

- sh: "gvm use go1.13"
- sh: "gvm use go1.15"
- sh: "echo $PATH"
- sh: "ls /usr/"
- sh: "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64"
Expand Down
2 changes: 1 addition & 1 deletion samcli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
SAM CLI version
"""

__version__ = "1.56.0"
__version__ = "1.56.1"
42 changes: 42 additions & 0 deletions samcli/lib/samlib/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
InvalidResourceException,
InvalidEventException,
)
from samtranslator.model.types import is_str
from samtranslator.plugins import LifeCycleEvents
from samtranslator.sdk.resource import SamResource, SamResourceType
from samtranslator.translator.translator import prepare_plugins
from samtranslator.validator.validator import SamTemplateValidator

Expand Down Expand Up @@ -64,6 +66,9 @@ def run_plugins(self, convert_local_uris=True):
additional_plugins, parameters=self.parameter_values if self.parameter_values else {}
)

# Temporarily disabling validation for DeletionPolicy and UpdateReplacePolicy when language extensions are set
self._patch_language_extensions()

try:
parser.parse(template_copy, all_plugins) # parse() will run all configured plugins
except InvalidDocumentException as e:
Expand All @@ -77,6 +82,43 @@ def run_plugins(self, convert_local_uris=True):
def template(self):
return copy.deepcopy(self._sam_template)

def _patch_language_extensions(self) -> None:
"""
Monkey patch SamResource.valid function to exclude checking DeletionPolicy
and UpdateReplacePolicy when language extensions are set
"""
template_copy = self.template
if self._check_using_language_extension(template_copy):

def patched_func(self):
if self.condition:
if not is_str()(self.condition, should_raise=False):
raise InvalidDocumentException(
[InvalidTemplateException("Every Condition member must be a string.")]
)
return SamResourceType.has_value(self.type)

SamResource.valid = patched_func

@staticmethod
def _check_using_language_extension(template: Dict) -> bool:
"""
Check if language extensions are set in the template's Transform
:param template: template to check
:return: True if language extensions are set in the template, False otherwise
"""
transform = template.get("Transform")
if transform:
if isinstance(transform, str) and transform.startswith("AWS::LanguageExtensions"):
return True
if isinstance(transform, list):
for transform_instance in transform:
if not isinstance(transform_instance, str):
continue
if transform_instance.startswith("AWS::LanguageExtensions"):
return True
return False


class _SamParserReimplemented:
"""
Expand Down
2 changes: 1 addition & 1 deletion samcli/lib/sync/watch_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def _start(self) -> None:
time.sleep(1)

def _execute_infra_sync(self) -> None:
LOG.info(self._color.cyan("Queued infra sync. Wating for in progress code syncs to complete..."))
LOG.info(self._color.cyan("Queued infra sync. Waiting for in progress code syncs to complete..."))
self._waiting_infra_sync = False
self._stop_code_sync()
try:
Expand Down
10 changes: 6 additions & 4 deletions samcli/lib/telemetry/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,12 @@ def wrapped(*args, **kwargs):
metric.add_data("debugFlagProvided", bool(ctx.debug))
metric.add_data("region", ctx.region or "")
metric.add_data("commandName", ctx.command_path) # Full command path. ex: sam local start-api
# Project metadata metrics
metric_specific_attributes["gitOrigin"] = get_git_remote_origin_url()
metric_specific_attributes["projectName"] = get_project_name()
metric_specific_attributes["initialCommit"] = get_initial_commit_hash()
if not ctx.command_path.endswith("init") or ctx.command_path.endswith("pipeline init"):
# Project metadata
# We don't capture below usage attributes for sam init as the command is not run inside a project
metric_specific_attributes["gitOrigin"] = get_git_remote_origin_url()
metric_specific_attributes["projectName"] = get_project_name()
metric_specific_attributes["initialCommit"] = get_initial_commit_hash()
metric.add_data("metricSpecificAttributes", metric_specific_attributes)
# Metric about command's execution characteristics
metric.add_data("duration", duration_fn())
Expand Down
66 changes: 42 additions & 24 deletions samcli/lib/telemetry/project_metadata.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
"""
Creates and encrypts metadata regarding SAM CLI projects.
Creates and hashes metadata regarding SAM CLI projects.
"""

import hashlib
from os import getcwd
import re
import subprocess
from typing import List, Optional
from os import getcwd
from os.path import basename
from typing import Optional
from urllib.parse import urlparse

from samcli.cli.global_config import GlobalConfig


def get_git_remote_origin_url() -> Optional[str]:
"""
Retrieve an encrypted version of the project's git remote origin url, if it exists.
Retrieve an hashed version of the project's git remote origin url, if it exists.
Returns
-------
str | None
A SHA256 hexdigest string of the git remote origin url, formatted such that the
encrypted value follows the pattern <hostname>/<owner>/<project_name>.git.
hashed value follows the pattern <hostname>/<owner>/<project_name>.git.
If telemetry is opted out of by the user, or the `.git` folder is not found
(the directory is not a git repository), returns None
"""
Expand All @@ -31,17 +33,17 @@ def get_git_remote_origin_url() -> Optional[str]:
runcmd = subprocess.run(
["git", "config", "--get", "remote.origin.url"], capture_output=True, shell=True, check=True, text=True
)
metadata = _parse_remote_origin_url(str(runcmd.stdout))
git_url = "/".join(metadata) + ".git" # Format to <hostname>/<owner>/<project_name>.git
git_url = _parse_remote_origin_url(str(runcmd.stdout))
except subprocess.CalledProcessError:
return None # Not a git repo
# Ignoring, None git_url will be handled later
pass

return _encrypt_value(git_url)
return _hash_value(git_url) if git_url else None


def get_project_name() -> Optional[str]:
"""
Retrieve an encrypted version of the project's name, as defined by the .git folder (or directory name if no .git).
Retrieve an hashed version of the project's name, as defined by the .git folder (or directory name if no .git).
Returns
-------
Expand All @@ -53,21 +55,27 @@ def get_project_name() -> Optional[str]:
if not bool(GlobalConfig().telemetry_enabled):
return None

project_name = ""
project_name = None
try:
runcmd = subprocess.run(
["git", "config", "--get", "remote.origin.url"], capture_output=True, shell=True, check=True, text=True
)
project_name = _parse_remote_origin_url(str(runcmd.stdout))[2] # dir is git repo, get project name from URL
git_url = _parse_remote_origin_url(str(runcmd.stdout))
if git_url:
project_name = git_url.split("/")[-1] # dir is git repo, get project name from URL
except subprocess.CalledProcessError:
project_name = getcwd().replace("\\", "/") # dir is not a git repo, get directory name
# Ignoring, None project_name will be handled at the end before returning
pass

if not project_name:
project_name = basename(getcwd().replace("\\", "/")) # dir is not a git repo, get directory name

return _encrypt_value(project_name)
return _hash_value(project_name)


def get_initial_commit_hash() -> Optional[str]:
"""
Retrieve an encrypted version of the project's initial commit hash, if it exists.
Retrieve an hashed version of the project's initial commit hash, if it exists.
Returns
-------
Expand All @@ -88,24 +96,34 @@ def get_initial_commit_hash() -> Optional[str]:
except subprocess.CalledProcessError:
return None # Not a git repo

return _encrypt_value(metadata)
return _hash_value(metadata)


def _parse_remote_origin_url(url: str) -> List[str]:
def _parse_remote_origin_url(url: str) -> Optional[str]:
"""
Parse a `git remote origin url` into its hostname, owner, and project name.
Parse a `git remote origin url` into a formatted "hostname/project" string
Returns
-------
List[str]
A list of 3 strings, with indeces corresponding to 0:hostname, 1:owner, 2:project_name
str
formatted project origin url
"""
pattern = re.compile(r"(?:https?://|git@)(?P<hostname>\S*)(?:/|:)(?P<owner>\S*)/(?P<project_name>\S*)\.git")
return [str(item) for item in pattern.findall(url)[0]]
parsed = urlparse(url)
if not parsed.path:
return None

formatted = (parsed.hostname or "") + parsed.path
formatted = re.sub(r"\n", "", formatted)
formatted = re.sub("/$", "", formatted)
formatted = re.sub(".git$", "", formatted)
formatted = re.sub("^(.+)@", "", formatted)
formatted = formatted.replace(":", "/")

return formatted


def _encrypt_value(value: str) -> str:
"""Encrypt a string, and then return the encrypted value as a byte string."""
def _hash_value(value: str) -> str:
"""Hash a string, and then return the hashed value as a byte string."""
h = hashlib.sha256()
h.update(value.encode("utf-8"))
return h.hexdigest()
3 changes: 3 additions & 0 deletions samcli/local/apigw/local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from flask import Flask, request
from werkzeug.datastructures import Headers
from werkzeug.routing import BaseConverter
from werkzeug.serving import WSGIRequestHandler

from samcli.lib.providers.provider import Cors
from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser
Expand Down Expand Up @@ -155,6 +156,8 @@ def create(self):
"""
Creates a Flask Application that can be started.
"""
# Setting sam local start-api to respond using HTTP/1.1 instead of the default HTTP/1.0
WSGIRequestHandler.protocol_version = "HTTP/1.1"

self._app = Flask(
__name__,
Expand Down
16 changes: 16 additions & 0 deletions tests/integration/buildcmd/test_build_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -2536,3 +2536,19 @@ def test_sar_application_with_location_resolved_from_map(self, use_container, re
# will fail the build as there is no mapping
self.assertEqual(process_execute.process.returncode, 1)
self.assertIn("Property \\'ApplicationId\\' cannot be resolved.", str(process_execute.stderr))


@skipIf(
((IS_WINDOWS and RUNNING_ON_CI) and not CI_OVERRIDE),
"Skip build tests on windows when running in CI unless overridden",
)
class TestBuildWithLanguageExtensions(BuildIntegBase):
template = "language-extensions.yaml"

def test_validation_does_not_error_out(self):
cmdlist = self.get_command_list()
LOG.info("Running Command: %s", cmdlist)
LOG.info(self.working_dir)
process_execute = run_command(cmdlist, cwd=self.working_dir)
self.assertEqual(process_execute.process.returncode, 0)
self.assertIn("template.yaml", os.listdir(self.default_build_dir))
11 changes: 11 additions & 0 deletions tests/integration/deploy/test_deploy_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -1582,3 +1582,14 @@ def test_update_stack_correct_stack_outputs(self, template):
process_stdout = deploy_process_execute.stdout.decode()
self.assertNotRegex(process_stdout, r"CREATE_COMPLETE.+HelloWorldFunction")
self.assertRegex(process_stdout, r"UPDATE_COMPLETE.+HelloWorldFunction")

def test_deploy_with_language_extensions(self):
template = Path(__file__).resolve().parents[1].joinpath("testdata", "buildcmd", "language-extensions.yaml")
stack_name = self._method_to_stack_name(self.id())
self.stacks.append({"name": stack_name})

deploy_command_list = self.get_deploy_command_list(
template_file=template, stack_name=stack_name, capabilities="CAPABILITY_IAM"
)
deploy_process_execute = run_command(deploy_command_list)
self.assertEqual(deploy_process_execute.process.returncode, 0)
Loading

0 comments on commit 1c9a2a5

Please sign in to comment.