Skip to content

Commit

Permalink
Merge pull request #1 from spulec/master
Browse files Browse the repository at this point in the history
updating with upstream changes
  • Loading branch information
georgeionita authored Jan 14, 2018
2 parents 3cede60 + 272b480 commit baba13c
Show file tree
Hide file tree
Showing 31 changed files with 1,296 additions and 53 deletions.
7 changes: 7 additions & 0 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[bumpversion]
current_version = 1.2.0

[bumpversion:file:setup.py]

[bumpversion:file:moto/__init__.py]

11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
Moto Changelog
===================

Latest
1.2.0
------

* Supports filtering AMIs by self
* Implemented signal_workflow_execution for SWF
* Wired SWF backend to the moto server
* Fixed incorrect handling of task list parameter on start_workflow_execution
* Revamped lambda function storage to do versioning
* IOT improvements
* RDS improvements
* Implemented CloudWatch get_metric_statistics
* Improved Cloudformation EC2 support
* Implemented Cloudformation change_set endpoints

1.1.25
-----
Expand Down
25 changes: 23 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
### Contributing code

If you have improvements to Moto, send us your pull requests! For those
just getting started, Github has a [howto](https://help.github.com/articles/using-pull-requests/).
Moto has a [Code of Conduct](https://github.com/spulec/moto/blob/master/CODE_OF_CONDUCT.md), you can expect to be treated with respect at all times when interacting with this project.

## Is there a missing feature?

Moto is easier to contribute to than you probably think. There's [a list of which endpoints have been implemented](https://github.com/spulec/moto/blob/master/IMPLEMENTATION_COVERAGE.md) and we invite you to add new endpoints to existing services or to add new services.

How to teach Moto to support a new AWS endpoint:

* Create an issue describing what's missing. This is where we'll all talk about the new addition and help you get it done.
* Create a [pull request](https://help.github.com/articles/using-pull-requests/) and mention the issue # in the PR description.
* Try to add a failing test case. For example, if you're trying to implement `boto3.client('acm').import_certificate()` you'll want to add a new method called `def test_import_certificate` to `tests/test_acm/test_acm.py`.
* If you can also implement the code that gets that test passing that's great. If not, just ask the community for a hand and somebody will assist you.

# Maintainers

## Releasing a new version of Moto

You'll need a PyPi account and a Dockerhub account to release Moto. After we release a new PyPi package we build and push the [motoserver/moto](https://hub.docker.com/r/motoserver/moto/) Docker image.

* First, `scripts/bump_version` modifies the version and opens a PR
* Then, merge the new pull request
* Finally, generate and ship the new artifacts with `make publish`

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ tag_github_release:
git tag `python setup.py --version`
git push origin `python setup.py --version`

publish: implementation_coverage \
publish:
upload_pypi_artifact \
tag_github_release \
push_dockerhub_image
Expand Down
2 changes: 1 addition & 1 deletion moto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# logging.getLogger('boto').setLevel(logging.CRITICAL)

__title__ = 'moto'
__version__ = '1.0.1'
__version__ = '1.2.0',

from .acm import mock_acm # flake8: noqa
from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa
Expand Down
3 changes: 2 additions & 1 deletion moto/cloudformation/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ def exports(self):
def update(self, template, role_arn=None, parameters=None, tags=None):
self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated")
self.template = template
self.resource_map.update(json.loads(template), parameters)
self._parse_template()
self.resource_map.update(self.template_dict, parameters)
self.output_map = self._create_output_map()
self._add_stack_event("UPDATE_COMPLETE")
self.status = "UPDATE_COMPLETE"
Expand Down
19 changes: 19 additions & 0 deletions moto/core/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class BaseResponse(_TemplateEnvironmentMixin):
# to extract region, use [^.]
region_regex = re.compile(r'\.(?P<region>[a-z]{2}-[a-z]+-\d{1})\.amazonaws\.com')
param_list_regex = re.compile(r'(.*)\.(\d+)\.')
access_key_regex = re.compile(r'AWS.*(?P<access_key>(?<![A-Z0-9])[A-Z0-9]{20}(?![A-Z0-9]))[:/]')
aws_service_spec = None

@classmethod
Expand Down Expand Up @@ -178,6 +179,21 @@ def get_region_from_url(self, request, full_url):
region = self.default_region
return region

def get_current_user(self):
"""
Returns the access key id used in this request as the current user id
"""
if 'Authorization' in self.headers:
match = self.access_key_regex.search(self.headers['Authorization'])
if match:
return match.group(1)

if self.querystring.get('AWSAccessKeyId'):
return self.querystring.get('AWSAccessKeyId')
else:
# Should we raise an unauthorized exception instead?
return None

def _dispatch(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
return self.call_action()
Expand Down Expand Up @@ -272,6 +288,9 @@ def call_action(self):
headers['status'] = str(headers['status'])
return status, headers, body

if not action:
return 404, headers, ''

raise NotImplementedError(
"The {0} action has not been implemented".format(action))

Expand Down
2 changes: 2 additions & 0 deletions moto/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def camelcase_to_underscores(argument):
python underscore variable like the_new_attribute'''
result = ''
prev_char_title = True
if not argument:
return argument
for index, char in enumerate(argument):
try:
next_char_title = argument[index + 1].istitle()
Expand Down
31 changes: 25 additions & 6 deletions moto/ec2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,6 @@ def describe_tags(self, filters=None):
class Ami(TaggedEC2Resource):
def __init__(self, ec2_backend, ami_id, instance=None, source_ami=None,
name=None, description=None, owner_id=None,

public=False, virtualization_type=None, architecture=None,
state='available', creation_date=None, platform=None,
image_type='machine', image_location=None, hypervisor=None,
Expand Down Expand Up @@ -1138,12 +1137,14 @@ def _load_amis(self):
ami_id = ami['ami_id']
self.amis[ami_id] = Ami(self, **ami)

def create_image(self, instance_id, name=None, description=None, owner_id=None):
def create_image(self, instance_id, name=None, description=None,
context=None):
# TODO: check that instance exists and pull info from it.
ami_id = random_ami_id()
instance = self.get_instance(instance_id)
ami = Ami(self, ami_id, instance=instance, source_ami=None,
name=name, description=description, owner_id=owner_id)
name=name, description=description,
owner_id=context.get_current_user() if context else None)
self.amis[ami_id] = ami
return ami

Expand All @@ -1156,7 +1157,8 @@ def copy_image(self, source_image_id, source_region, name=None, description=None
self.amis[ami_id] = ami
return ami

def describe_images(self, ami_ids=(), filters=None, exec_users=None, owners=None):
def describe_images(self, ami_ids=(), filters=None, exec_users=None, owners=None,
context=None):
images = self.amis.values()

# Limit images by launch permissions
Expand All @@ -1170,6 +1172,11 @@ def describe_images(self, ami_ids=(), filters=None, exec_users=None, owners=None

# Limit by owner ids
if owners:
# support filtering by Owners=['self']
owners = list(map(
lambda o: context.get_current_user()
if context and o == 'self' else o,
owners))
images = [ami for ami in images if ami.owner_id in owners]

if ami_ids:
Expand Down Expand Up @@ -1261,8 +1268,15 @@ class RegionsAndZonesBackend(object):
(region, [Zone(region + c, region) for c in 'abc'])
for region in [r.name for r in regions])

def describe_regions(self):
return self.regions
def describe_regions(self, region_names=[]):
if len(region_names) == 0:
return self.regions
ret = []
for name in region_names:
for region in self.regions:
if region.name == name:
ret.append(region)
return ret

def describe_availability_zones(self):
return self.zones[self.region_name]
Expand Down Expand Up @@ -2004,6 +2018,11 @@ def create_from_cloudformation_json(cls, resource_name, cloudformation_json, reg
cidr_block=properties['CidrBlock'],
instance_tenancy=properties.get('InstanceTenancy', 'default')
)
for tag in properties.get("Tags", []):
tag_key = tag["Key"]
tag_value = tag["Value"]
vpc.add_tag(tag_key, tag_value)

return vpc

@property
Expand Down
5 changes: 3 additions & 2 deletions moto/ec2/responses/amis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def create_image(self):
instance_id = self._get_param('InstanceId')
if self.is_not_dryrun('CreateImage'):
image = self.ec2_backend.create_image(
instance_id, name, description)
instance_id, name, description, context=self)
template = self.response_template(CREATE_IMAGE_RESPONSE)
return template.render(image=image)

Expand Down Expand Up @@ -39,7 +39,8 @@ def describe_images(self):
owners = self._get_multi_param('Owner')
exec_users = self._get_multi_param('ExecutableBy')
images = self.ec2_backend.describe_images(
ami_ids=ami_ids, filters=filters, exec_users=exec_users, owners=owners)
ami_ids=ami_ids, filters=filters, exec_users=exec_users,
owners=owners, context=self)
template = self.response_template(DESCRIBE_IMAGES_RESPONSE)
return template.render(images=images)

Expand Down
3 changes: 2 additions & 1 deletion moto/ec2/responses/availability_zones_and_regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ def describe_availability_zones(self):
return template.render(zones=zones)

def describe_regions(self):
regions = self.ec2_backend.describe_regions()
region_names = self._get_multi_param('RegionName')
regions = self.ec2_backend.describe_regions(region_names)
template = self.response_template(DESCRIBE_REGIONS_RESPONSE)
return template.render(regions=regions)

Expand Down
52 changes: 48 additions & 4 deletions moto/ecr/models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from __future__ import unicode_literals
# from datetime import datetime

import hashlib
from copy import copy
from random import random

from moto.core import BaseBackend, BaseModel
from moto.ec2 import ec2_backends
from copy import copy
import hashlib

from moto.ecr.exceptions import ImageNotFoundException, RepositoryNotFoundException

from botocore.exceptions import ParamValidationError

DEFAULT_REGISTRY_ID = '012345678910'

Expand Down Expand Up @@ -145,6 +145,17 @@ def response_describe_object(self):
response_object['imagePushedAt'] = '2017-05-09'
return response_object

@property
def response_batch_get_image(self):
response_object = {}
response_object['imageId'] = {}
response_object['imageId']['imageTag'] = self.image_tag
response_object['imageId']['imageDigest'] = self.get_image_digest()
response_object['imageManifest'] = self.image_manifest
response_object['repositoryName'] = self.repository
response_object['registryId'] = self.registry_id
return response_object


class ECRBackend(BaseBackend):

Expand Down Expand Up @@ -245,6 +256,39 @@ def put_image(self, repository_name, image_manifest, image_tag):
repository.images.append(image)
return image

def batch_get_image(self, repository_name, registry_id=None, image_ids=None, accepted_media_types=None):
if repository_name in self.repositories:
repository = self.repositories[repository_name]
else:
raise RepositoryNotFoundException(repository_name, registry_id or DEFAULT_REGISTRY_ID)

if not image_ids:
raise ParamValidationError(msg='Missing required parameter in input: "imageIds"')

response = {
'images': [],
'failures': [],
}

for image_id in image_ids:
found = False
for image in repository.images:
if (('imageDigest' in image_id and image.get_image_digest() == image_id['imageDigest']) or
('imageTag' in image_id and image.image_tag == image_id['imageTag'])):
found = True
response['images'].append(image.response_batch_get_image)

if not found:
response['failures'].append({
'imageId': {
'imageTag': image_id.get('imageTag', 'null')
},
'failureCode': 'ImageNotFound',
'failureReason': 'Requested image not found'
})

return response


ecr_backends = {}
for region, ec2_backend in ec2_backends.items():
Expand Down
10 changes: 7 additions & 3 deletions moto/ecr/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,13 @@ def batch_delete_image(self):
'ECR.batch_delete_image is not yet implemented')

def batch_get_image(self):
if self.is_not_dryrun('BatchGetImage'):
raise NotImplementedError(
'ECR.batch_get_image is not yet implemented')
repository_str = self._get_param('repositoryName')
registry_id = self._get_param('registryId')
image_ids = self._get_param('imageIds')
accepted_media_types = self._get_param('acceptedMediaTypes')

response = self.ecr_backend.batch_get_image(repository_str, registry_id, image_ids, accepted_media_types)
return json.dumps(response)

def can_paginate(self):
if self.is_not_dryrun('CanPaginate'):
Expand Down
12 changes: 12 additions & 0 deletions moto/iam/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,14 @@ def delete_access_key(self, access_key_id):
raise IAMNotFoundException(
"Key {0} not found".format(access_key_id))

def update_access_key(self, access_key_id, status):
for key in self.access_keys:
if key.access_key_id == access_key_id:
key.status = status
break
else:
raise IAMNotFoundException("The Access Key with id {0} cannot be found".format(access_key_id))

def get_cfn_attribute(self, attribute_name):
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
if attribute_name == 'Arn':
Expand Down Expand Up @@ -817,6 +825,10 @@ def create_access_key(self, user_name=None):
key = user.create_access_key()
return key

def update_access_key(self, user_name, access_key_id, status):
user = self.get_user(user_name)
user.update_access_key(access_key_id, status)

def get_all_access_keys(self, user_name, marker=None, max_items=None):
user = self.get_user(user_name)
keys = user.get_all_access_keys()
Expand Down
8 changes: 8 additions & 0 deletions moto/iam/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,14 @@ def create_access_key(self):
template = self.response_template(CREATE_ACCESS_KEY_TEMPLATE)
return template.render(key=key)

def update_access_key(self):
user_name = self._get_param('UserName')
access_key_id = self._get_param('AccessKeyId')
status = self._get_param('Status')
iam_backend.update_access_key(user_name, access_key_id, status)
template = self.response_template(GENERIC_EMPTY_TEMPLATE)
return template.render(name='UpdateAccessKey')

def list_access_keys(self):
user_name = self._get_param('UserName')

Expand Down
12 changes: 10 additions & 2 deletions moto/iot/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@ def __init__(self):


class InvalidRequestException(IoTClientError):
def __init__(self):
def __init__(self, msg=None):
self.code = 400
super(InvalidRequestException, self).__init__(
"InvalidRequestException",
"The request is not valid."
msg or "The request is not valid."
)


class VersionConflictException(IoTClientError):
def __init__(self, name):
self.code = 409
super(VersionConflictException, self).__init__(
'The version for thing %s does not match the expected version.' % name
)
Loading

0 comments on commit baba13c

Please sign in to comment.