Skip to content

Commit

Permalink
Merge remote master
Browse files Browse the repository at this point in the history
  • Loading branch information
shmygol committed Dec 11, 2019
2 parents be5986c + 4d5bf1c commit 3a42079
Show file tree
Hide file tree
Showing 45 changed files with 1,642 additions and 192 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ deploy:
on:
branch:
- master
cleanup: false
skip_cleanup: true
skip_existing: true
# - provider: pypi
# distributions: sdist bdist_wheel
Expand Down
6 changes: 3 additions & 3 deletions IMPLEMENTATION_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4850,12 +4850,12 @@
- [X] list_policies
- [X] list_policies_for_target
- [X] list_roots
- [ ] list_tags_for_resource
- [x] list_tags_for_resource
- [X] list_targets_for_policy
- [X] move_account
- [ ] remove_account_from_organization
- [ ] tag_resource
- [ ] untag_resource
- [x] tag_resource
- [x] untag_resource
- [ ] update_organizational_unit
- [ ] update_policy

Expand Down
31 changes: 31 additions & 0 deletions moto/awslambda/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from botocore.client import ClientError


class LambdaClientError(ClientError):
def __init__(self, error, message):
error_response = {"Error": {"Code": error, "Message": message}}
super(LambdaClientError, self).__init__(error_response, None)


class CrossAccountNotAllowed(LambdaClientError):
def __init__(self):
super(CrossAccountNotAllowed, self).__init__(
"AccessDeniedException", "Cross-account pass role is not allowed."
)


class InvalidParameterValueException(LambdaClientError):
def __init__(self, message):
super(InvalidParameterValueException, self).__init__(
"InvalidParameterValueException", message
)


class InvalidRoleFormat(LambdaClientError):
pattern = r"arn:(aws[a-zA-Z-]*)?:iam::(\d{12}):role/?[a-zA-Z_0-9+=,.@\-_/]+"

def __init__(self, role):
message = "1 validation error detected: Value '{0}' at 'role' failed to satisfy constraint: Member must satisfy regular expression pattern: {1}".format(
role, InvalidRoleFormat.pattern
)
super(InvalidRoleFormat, self).__init__("ValidationException", message)
39 changes: 36 additions & 3 deletions moto/awslambda/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@
import boto.awslambda
from moto.core import BaseBackend, BaseModel
from moto.core.exceptions import RESTError
from moto.iam.models import iam_backend
from moto.iam.exceptions import IAMNotFoundException
from moto.core.utils import unix_time_millis
from moto.s3.models import s3_backend
from moto.logs.models import logs_backends
from moto.s3.exceptions import MissingBucket, MissingKey
from moto import settings
from .exceptions import (
CrossAccountNotAllowed,
InvalidRoleFormat,
InvalidParameterValueException,
)
from .utils import make_function_arn, make_function_ver_arn
from moto.sqs import sqs_backends
from moto.dynamodb2 import dynamodb_backends2
Expand Down Expand Up @@ -214,9 +221,8 @@ def replace_adapter_send(*args, **kwargs):
key = s3_backend.get_key(self.code["S3Bucket"], self.code["S3Key"])
except MissingBucket:
if do_validate_s3():
raise ValueError(
"InvalidParameterValueException",
"Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist",
raise InvalidParameterValueException(
"Error occurred while GetObject. S3 Error Code: NoSuchBucket. S3 Error Message: The specified bucket does not exist"
)
except MissingKey:
if do_validate_s3():
Expand Down Expand Up @@ -357,6 +363,8 @@ def update_function_code(self, updated_spec):
self.code_bytes = key.value
self.code_size = key.size
self.code_sha_256 = hashlib.sha256(key.value).hexdigest()
self.code["S3Bucket"] = updated_spec["S3Bucket"]
self.code["S3Key"] = updated_spec["S3Key"]

return self.get_configuration()

Expand Down Expand Up @@ -520,6 +528,15 @@ def get_cfn_attribute(self, attribute_name):
return make_function_arn(self.region, ACCOUNT_ID, self.function_name)
raise UnformattedGetAttTemplateException()

@classmethod
def update_from_cloudformation_json(
cls, new_resource_name, cloudformation_json, original_resource, region_name
):
updated_props = cloudformation_json["Properties"]
original_resource.update_configuration(updated_props)
original_resource.update_function_code(updated_props["Code"])
return original_resource

@staticmethod
def _create_zipfile_from_plaintext_code(code):
zip_output = io.BytesIO()
Expand All @@ -529,6 +546,9 @@ def _create_zipfile_from_plaintext_code(code):
zip_output.seek(0)
return zip_output.read()

def delete(self, region):
lambda_backends[region].delete_function(self.function_name)


class EventSourceMapping(BaseModel):
def __init__(self, spec):
Expand Down Expand Up @@ -668,6 +688,19 @@ def put_function(self, fn):
:param fn: Function
:type fn: LambdaFunction
"""
valid_role = re.match(InvalidRoleFormat.pattern, fn.role)
if valid_role:
account = valid_role.group(2)
if account != ACCOUNT_ID:
raise CrossAccountNotAllowed()
try:
iam_backend.get_role_by_arn(fn.role)
except IAMNotFoundException:
raise InvalidParameterValueException(
"The role defined for the function cannot be assumed by Lambda."
)
else:
raise InvalidRoleFormat(fn.role)
if fn.function_name in self._functions:
self._functions[fn.function_name]["latest"] = fn
else:
Expand Down
28 changes: 6 additions & 22 deletions moto/awslambda/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,30 +211,14 @@ def _list_versions_by_function(self, function_name):
return 200, {}, json.dumps(result)

def _create_function(self, request, full_url, headers):
try:
fn = self.lambda_backend.create_function(self.json_body)
except ValueError as e:
return (
400,
{},
json.dumps({"Error": {"Code": e.args[0], "Message": e.args[1]}}),
)
else:
config = fn.get_configuration()
return 201, {}, json.dumps(config)
fn = self.lambda_backend.create_function(self.json_body)
config = fn.get_configuration()
return 201, {}, json.dumps(config)

def _create_event_source_mapping(self, request, full_url, headers):
try:
fn = self.lambda_backend.create_event_source_mapping(self.json_body)
except ValueError as e:
return (
400,
{},
json.dumps({"Error": {"Code": e.args[0], "Message": e.args[1]}}),
)
else:
config = fn.get_configuration()
return 201, {}, json.dumps(config)
fn = self.lambda_backend.create_event_source_mapping(self.json_body)
config = fn.get_configuration()
return 201, {}, json.dumps(config)

def _list_event_source_mappings(self, event_source_arn, function_name):
esms = self.lambda_backend.list_event_source_mappings(
Expand Down
32 changes: 32 additions & 0 deletions moto/cloudwatch/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import boto.ec2.cloudwatch
from datetime import datetime, timedelta
from dateutil.tz import tzutc
from uuid import uuid4
from .utils import make_arn_for_dashboard

DEFAULT_ACCOUNT_ID = 123456789012
Expand Down Expand Up @@ -193,6 +194,7 @@ def __init__(self):
self.alarms = {}
self.dashboards = {}
self.metric_data = []
self.paged_metric_data = {}

def put_metric_alarm(
self,
Expand Down Expand Up @@ -377,6 +379,36 @@ def set_alarm_state(self, alarm_name, reason, reason_data, state_value):

self.alarms[alarm_name].update_state(reason, reason_data, state_value)

def list_metrics(self, next_token, namespace, metric_name):
if next_token:
if next_token not in self.paged_metric_data:
raise RESTError(
"PaginationException", "Request parameter NextToken is invalid"
)
else:
metrics = self.paged_metric_data[next_token]
del self.paged_metric_data[next_token] # Cant reuse same token twice
return self._get_paginated(metrics)
else:
metrics = self.get_filtered_metrics(metric_name, namespace)
return self._get_paginated(metrics)

def get_filtered_metrics(self, metric_name, namespace):
metrics = self.get_all_metrics()
if namespace:
metrics = [md for md in metrics if md.namespace == namespace]
if metric_name:
metrics = [md for md in metrics if md.name == metric_name]
return metrics

def _get_paginated(self, metrics):
if len(metrics) > 500:
next_token = str(uuid4())
self.paged_metric_data[next_token] = metrics[500:]
return next_token, metrics[0:500]
else:
return None, metrics


class LogGroup(BaseModel):
def __init__(self, spec):
Expand Down
13 changes: 10 additions & 3 deletions moto/cloudwatch/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,14 @@ def get_metric_statistics(self):

@amzn_request_id
def list_metrics(self):
metrics = self.cloudwatch_backend.get_all_metrics()
namespace = self._get_param("Namespace")
metric_name = self._get_param("MetricName")
next_token = self._get_param("NextToken")
next_token, metrics = self.cloudwatch_backend.list_metrics(
next_token, namespace, metric_name
)
template = self.response_template(LIST_METRICS_TEMPLATE)
return template.render(metrics=metrics)
return template.render(metrics=metrics, next_token=next_token)

@amzn_request_id
def delete_dashboards(self):
Expand Down Expand Up @@ -340,9 +345,11 @@ def set_alarm_state(self):
</member>
{% endfor %}
</Metrics>
{% if next_token is not none %}
<NextToken>
96e88479-4662-450b-8a13-239ded6ce9fe
{{ next_token }}
</NextToken>
{% endif %}
</ListMetricsResult>
</ListMetricsResponse>"""

Expand Down
2 changes: 1 addition & 1 deletion moto/core/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def uri_to_regexp(self, uri):
def _convert(elem, is_last):
if not re.match("^{.*}$", elem):
return elem
name = elem.replace("{", "").replace("}", "")
name = elem.replace("{", "").replace("}", "").replace("+", "")
if is_last:
return "(?P<%s>[^/]*)" % name
return "(?P<%s>.*)" % name
Expand Down
6 changes: 5 additions & 1 deletion moto/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import six
import string
from botocore.exceptions import ClientError
from six.moves.urllib.parse import urlparse


Expand Down Expand Up @@ -141,7 +142,10 @@ def __name__(self):
def __call__(self, args=None, **kwargs):
from flask import request, Response

result = self.callback(request, request.url, {})
try:
result = self.callback(request, request.url, {})
except ClientError as exc:
result = 400, {}, exc.response["Error"]["Message"]
# result is a status, headers, response tuple
if len(result) == 3:
status, headers, content = result
Expand Down
2 changes: 1 addition & 1 deletion moto/datasync/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def describe_task_execution(self):
task_execution_arn = self._get_param("TaskExecutionArn")
task_execution = self.datasync_backend._get_task_execution(task_execution_arn)
result = json.dumps(
{"TaskExecutionArn": task_execution.arn, "Status": task_execution.status,}
{"TaskExecutionArn": task_execution.arn, "Status": task_execution.status}
)
if task_execution.status == "SUCCESS":
self.datasync_backend.tasks[task_execution.task_arn].status = "AVAILABLE"
Expand Down
4 changes: 1 addition & 3 deletions moto/datasync/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,4 @@

url_bases = ["https?://(.*?)(datasync)(.*?).amazonaws.com"]

url_paths = {
"{0}/$": DataSyncResponse.dispatch,
}
url_paths = {"{0}/$": DataSyncResponse.dispatch}
Loading

0 comments on commit 3a42079

Please sign in to comment.