From 5324638573e18e51b895b39ec0d32ac2133a37d0 Mon Sep 17 00:00:00 2001 From: Steve Pulec Date: Sun, 26 Feb 2017 19:55:19 -0500 Subject: [PATCH 01/15] Add docs on contributing and code of conduct. --- CODE_OF_CONDUCT.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 4 +++ ISSUE_TEMPLATE.md | 33 +++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 ISSUE_TEMPLATE.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..8f2d40361 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project maintainer at spulec@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1266d508e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +### 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/). diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..c3d7d3f65 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,33 @@ +## Reporting Bugs + +Please be aware of the following things when filing bug reports: + +1. Avoid raising duplicate issues. *Please* use the GitHub issue search feature + to check whether your bug report or feature request has been mentioned in + the past. +2. When filing bug reports about exceptions or tracebacks, please include the + *complete* traceback. Partial tracebacks, or just the exception text, are + not helpful. +3. Make sure you provide a suitable amount of information to work with. This + means you should provide: + + - Guidance on **how to reproduce the issue**. Ideally, this should be a + *small* code sample that can be run immediately by the maintainers. + Failing that, let us know what you're doing, how often it happens, what + environment you're using, etc. Be thorough: it prevents us needing to ask + further questions. + - Tell us **what you expected to happen**. When we run your example code, + what are we expecting to happen? What does "success" look like for your + code? + - Tell us **what actually happens**. It's not helpful for you to say "it + doesn't work" or "it fails". Tell us *how* it fails: do you get an + exception? A hang? How was the actual result different from your expected + result? + - Tell us **what version of Moto you're using**, and + **how you installed it**. Tell us whether you're using standalone server + mode or the Python mocks. If you are using the Python mocks, include the + version of boto/boto3/botocore. + + + If you do not provide all of these things, it will take us much longer to + fix your problem. From bcc3e57949831b31661c1c91d739128e8bbd56d9 Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:26:23 +1100 Subject: [PATCH 02/15] Cloudformation ResourceMaps incorrectly share namespaces for Conditions and Resources (#828) * add tests to check CF's conditions and resources have distinct namespace * separate the resource and condition namespaces for CF --- moto/cloudformation/parsing.py | 14 ++--- .../test_cloudformation_stack_crud.py | 54 +++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index 521658cee..fdc569dc1 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -143,7 +143,7 @@ def clean_json(resource_json, resources_map): if 'Fn::If' in resource_json: condition_name, true_value, false_value = resource_json['Fn::If'] - if resources_map[condition_name]: + if resources_map.lazy_condition_map[condition_name]: return clean_json(true_value, resources_map) else: return clean_json(false_value, resources_map) @@ -206,7 +206,7 @@ def parse_resource(logical_id, resource_json, resources_map): def parse_and_create_resource(logical_id, resource_json, resources_map, region_name): condition = resource_json.get('Condition') - if condition and not resources_map[condition]: + if condition and not resources_map.lazy_condition_map[condition]: # If this has a False condition, don't create the resource return None @@ -352,13 +352,13 @@ class ResourceMap(collections.Mapping): def load_conditions(self): conditions = self._template.get('Conditions', {}) - lazy_condition_map = LazyDict() + self.lazy_condition_map = LazyDict() for condition_name, condition in conditions.items(): - lazy_condition_map[condition_name] = functools.partial(parse_condition, - condition, self._parsed_resources, lazy_condition_map) + self.lazy_condition_map[condition_name] = functools.partial(parse_condition, + condition, self._parsed_resources, self.lazy_condition_map) - for condition_name in lazy_condition_map: - self._parsed_resources[condition_name] = lazy_condition_map[condition_name] + for condition_name in self.lazy_condition_map: + _ = self.lazy_condition_map[condition_name] def create(self): self.load_mapping() diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index e45dafbfa..0696d5ada 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -279,6 +279,60 @@ def test_cloudformation_params(): param.value.should.equal('testing123') +@mock_cloudformation() +def test_cloudformation_params_conditions_and_resources_are_distinct(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Conditions": { + "FooEnabled": { + "Fn::Equals": [ + { + "Ref": "FooEnabled" + }, + "true" + ] + }, + "FooDisabled": { + "Fn::Not": [ + { + "Fn::Equals": [ + { + "Ref": "FooEnabled" + }, + "true" + ] + } + ] + } + }, + "Parameters": { + "FooEnabled": { + "Type": "String", + "AllowedValues": [ + "true", + "false" + ] + } + }, + "Resources": { + "Bar": { + "Properties": { + "CidrBlock": "192.168.0.0/16", + }, + "Condition": "FooDisabled", + "Type": "AWS::EC2::VPC" + } + } + } + dummy_template_json = json.dumps(dummy_template) + cfn = boto.connect_cloudformation() + cfn.create_stack('test_stack1', template_body=dummy_template_json, parameters=[('FooEnabled', 'true')]) + stack = cfn.describe_stacks('test_stack1')[0] + resources = stack.list_resources() + assert not [resource for resource in resources if resource.logical_resource_id == 'Bar'] + + @mock_cloudformation def test_stack_tags(): conn = boto.connect_cloudformation() From 7d75c3ba189d41f35d23469fc47d1211ff3a3231 Mon Sep 17 00:00:00 2001 From: Guy Templeton Date: Sun, 5 Mar 2017 03:30:36 +0000 Subject: [PATCH 03/15] Feat: ECS container status updating (#831) * Uptick boto3 version to version supporting ECS container instance state changes * Add initial status update * Only place tasks on active instances * PEP8 cleanup --- moto/ecs/models.py | 47 ++++++++++++++++------------ moto/ecs/responses.py | 42 ++++++++++++------------- requirements-dev.txt | 2 +- tests/test_ecs/test_ecs_boto3.py | 53 ++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 41 deletions(-) diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 3ce7be8b5..25fe0ffec 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -61,6 +61,7 @@ class Cluster(BaseObject): # ClusterName is optional in CloudFormation, thus create a random name if necessary cluster_name=properties.get('ClusterName', 'ecscluster{0}'.format(int(random() * 10 ** 6))), ) + @classmethod def update_from_cloudformation_json(cls, original_resource, new_resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] @@ -126,6 +127,7 @@ class TaskDefinition(BaseObject): # no-op when nothing changed between old and new resources return original_resource + class Task(BaseObject): def __init__(self, cluster, task_definition, container_instance_arn, overrides={}, started_by=''): self.cluster_arn = cluster.arn @@ -227,10 +229,10 @@ class ContainerInstance(BaseObject): self.remainingResources = [] self.runningTaskCount = 0 self.versionInfo = { - 'agentVersion': "1.0.0", - 'agentHash': '4023248', - 'dockerVersion': 'DockerVersion: 1.5.0' - } + 'agentVersion': "1.0.0", + 'agentHash': '4023248', + 'dockerVersion': 'DockerVersion: 1.5.0' + } @property def response_object(self): @@ -327,20 +329,6 @@ class EC2ContainerServiceBackend(BaseBackend): task_arns.extend([task_definition.arn for task_definition in task_definition_list]) return task_arns - def describe_task_definition(self, task_definition_str): - task_definition_name = task_definition_str.split('/')[-1] - if ':' in task_definition_name: - family, revision = task_definition_name.split(':') - revision = int(revision) - else: - family = task_definition_name - revision = len(self.task_definitions.get(family, [])) - - if family in self.task_definitions and 0 < revision <= len(self.task_definitions[family]): - return self.task_definitions[family][revision-1] - else: - raise Exception("{0} is not a task_definition".format(task_definition_name)) - def deregister_task_definition(self, task_definition_str): task_definition_name = task_definition_str.split('/')[-1] family, revision = task_definition_name.split(':') @@ -363,9 +351,11 @@ class EC2ContainerServiceBackend(BaseBackend): container_instances = list(self.container_instances.get(cluster_name, {}).keys()) if not container_instances: raise Exception("No instances found in cluster {}".format(cluster_name)) + active_container_instances = [x for x in container_instances if + self.container_instances[cluster_name][x].status == 'ACTIVE'] for _ in range(count or 1): container_instance_arn = self.container_instances[cluster_name][ - container_instances[randint(0, len(container_instances) - 1)] + active_container_instances[randint(0, len(active_container_instances) - 1)] ].containerInstanceArn task = Task(cluster, task_definition, container_instance_arn, overrides or {}, started_by or '') tasks.append(task) @@ -537,6 +527,25 @@ class EC2ContainerServiceBackend(BaseBackend): return container_instance_objects, failures + def update_container_instances_state(self, cluster_str, list_container_instance_ids, status): + cluster_name = cluster_str.split('/')[-1] + if cluster_name not in self.clusters: + raise Exception("{0} is not a cluster".format(cluster_name)) + status = status.upper() + if status not in ['ACTIVE', 'DRAINING']: + raise Exception("An error occurred (InvalidParameterException) when calling the UpdateContainerInstancesState operation: Container instances status should be one of [ACTIVE,DRAINING]") + failures = [] + container_instance_objects = [] + for container_instance_id in list_container_instance_ids: + container_instance = self.container_instances[cluster_name].get(container_instance_id, None) + if container_instance is not None: + container_instance.status = status + container_instance_objects.append(container_instance) + else: + failures.append(ContainerInstanceFailure('MISSING', container_instance_id)) + + return container_instance_objects, failures + def deregister_container_instance(self, cluster_str, container_instance_str): pass diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index ce90de379..d61b7dd15 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals import json -import uuid from moto.core.responses import BaseResponse from .models import ecs_backends @@ -34,8 +33,8 @@ class EC2ContainerServiceResponse(BaseResponse): cluster_arns = self.ecs_backend.list_clusters() return json.dumps({ 'clusterArns': cluster_arns - #, - #'nextToken': str(uuid.uuid1()) + # , + # 'nextToken': str(uuid.uuid1()) }) def describe_clusters(self): @@ -66,15 +65,8 @@ class EC2ContainerServiceResponse(BaseResponse): task_definition_arns = self.ecs_backend.list_task_definitions() return json.dumps({ 'taskDefinitionArns': task_definition_arns - #, - #'nextToken': str(uuid.uuid1()) - }) - - def describe_task_definition(self): - task_definition_str = self._get_param('taskDefinition') - task_definition = self.ecs_backend.describe_task_definition(task_definition_str) - return json.dumps({ - 'taskDefinition': task_definition.response_object + # , + # 'nextToken': str(uuid.uuid1()) }) def deregister_task_definition(self): @@ -94,7 +86,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({ 'tasks': [task.response_object for task in tasks], 'failures': [] - }) + }) def describe_tasks(self): cluster = self._get_param('cluster') @@ -123,7 +115,7 @@ class EC2ContainerServiceResponse(BaseResponse): return json.dumps({ 'tasks': [task.response_object for task in tasks], 'failures': [] - }) + }) def list_tasks(self): cluster_str = self._get_param('cluster') @@ -135,8 +127,7 @@ class EC2ContainerServiceResponse(BaseResponse): task_arns = self.ecs_backend.list_tasks(cluster_str, container_instance, family, started_by, service_name, desiredStatus) return json.dumps({ 'taskArns': task_arns - }) - + }) def stop_task(self): cluster_str = self._get_param('cluster') @@ -145,8 +136,7 @@ class EC2ContainerServiceResponse(BaseResponse): task = self.ecs_backend.stop_task(cluster_str, task, reason) return json.dumps({ 'task': task.response_object - }) - + }) def create_service(self): cluster_str = self._get_param('cluster') @@ -201,7 +191,7 @@ class EC2ContainerServiceResponse(BaseResponse): ec2_instance_id = instance_identity_document["instanceId"] container_instance = self.ecs_backend.register_container_instance(cluster_str, ec2_instance_id) return json.dumps({ - 'containerInstance' : container_instance.response_object + 'containerInstance': container_instance.response_object }) def list_container_instances(self): @@ -216,6 +206,16 @@ class EC2ContainerServiceResponse(BaseResponse): list_container_instance_arns = self._get_param('containerInstances') container_instances, failures = self.ecs_backend.describe_container_instances(cluster_str, list_container_instance_arns) return json.dumps({ - 'failures': [ci.response_object for ci in failures], - 'containerInstances': [ci.response_object for ci in container_instances] + 'failures': [ci.response_object for ci in failures], + 'containerInstances': [ci.response_object for ci in container_instances] + }) + + def update_container_instances_state(self): + cluster_str = self._get_param('cluster') + list_container_instance_arns = self._get_param('containerInstances') + status_str = self._get_param('status') + container_instances, failures = self.ecs_backend.update_container_instances_state(cluster_str, list_container_instance_arns, status_str) + return json.dumps({ + 'failures': [ci.response_object for ci in failures], + 'containerInstances': [ci.response_object for ci in container_instances] }) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9bdccc6e4..554834a51 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,6 @@ sure==1.2.24 coverage freezegun flask -boto3>=1.3.1 +boto3>=1.4.4 botocore>=1.4.28 six diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index f073628a9..bbb86dbe3 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -573,6 +573,58 @@ def test_describe_container_instances(): for arn in test_instance_arns: response_arns.should.contain(arn) +@mock_ec2 +@mock_ecs +def test_update_container_instances_state(): + ecs_client = boto3.client('ecs', region_name='us-east-1') + ec2 = boto3.resource('ec2', region_name='us-east-1') + + test_cluster_name = 'test_ecs_cluster' + _ = ecs_client.create_cluster( + clusterName=test_cluster_name + ) + + instance_to_create = 3 + test_instance_arns = [] + for i in range(0, instance_to_create): + test_instance = ec2.create_instances( + ImageId="ami-1234abcd", + MinCount=1, + MaxCount=1, + )[0] + + instance_id_document = json.dumps( + ec2_utils.generate_instance_identity_document(test_instance) + ) + + response = ecs_client.register_container_instance( + cluster=test_cluster_name, + instanceIdentityDocument=instance_id_document) + + test_instance_arns.append(response['containerInstance']['containerInstanceArn']) + + test_instance_ids = list(map((lambda x: x.split('/')[1]), test_instance_arns)) + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING') + len(response['failures']).should.equal(0) + len(response['containerInstances']).should.equal(instance_to_create) + response_statuses = [ci['status'] for ci in response['containerInstances']] + for status in response_statuses: + status.should.equal('DRAINING') + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='DRAINING') + len(response['failures']).should.equal(0) + len(response['containerInstances']).should.equal(instance_to_create) + response_statuses = [ci['status'] for ci in response['containerInstances']] + for status in response_statuses: + status.should.equal('DRAINING') + response = ecs_client.update_container_instances_state(cluster=test_cluster_name, containerInstances=test_instance_ids, status='ACTIVE') + len(response['failures']).should.equal(0) + len(response['containerInstances']).should.equal(instance_to_create) + response_statuses = [ci['status'] for ci in response['containerInstances']] + for status in response_statuses: + status.should.equal('ACTIVE') + ecs_client.update_container_instances_state.when.called_with(cluster=test_cluster_name, containerInstances=test_instance_ids, status='test_status').should.throw(Exception) + + @mock_ec2 @mock_ecs @@ -861,6 +913,7 @@ def describe_task_definition(): task['taskDefinitionArn'].should.equal('arn:aws:ecs:us-east-1:012345678910:task-definition/test_ecs_task2:1') task['volumes'].should.equal([]) + @mock_ec2 @mock_ecs def test_stop_task(): From 8d737eb59d031b2e00a4dd32b5c3a8ac6af3af69 Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:31:45 +1100 Subject: [PATCH 04/15] Route53: allow hosted zone id as well when creating record sets (#833) * add test that creates r53 record set from hosted zone id (not name) * pass test to enable creating record sets by hosted zone ids --- moto/route53/models.py | 7 ++- .../test_cloudformation_stack_crud.py | 53 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/moto/route53/models.py b/moto/route53/models.py index 6b293a1ca..552deebdf 100644 --- a/moto/route53/models.py +++ b/moto/route53/models.py @@ -213,8 +213,11 @@ class RecordSetGroup(object): def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] - zone_name = properties["HostedZoneName"] - hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) + zone_name = properties.get("HostedZoneName") + if zone_name: + hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name) + else: + hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"]) record_sets = properties["RecordSets"] for record_set in record_sets: hosted_zone.add_rrset(record_set) diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 0696d5ada..a2b5a06f5 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -12,7 +12,7 @@ import sure # noqa import tests.backport_assert_raises # noqa from nose.tools import assert_raises -from moto import mock_cloudformation, mock_s3 +from moto import mock_cloudformation, mock_s3, mock_route53 from moto.cloudformation import cloudformation_backends dummy_template = { @@ -69,6 +69,57 @@ def test_create_stack(): }) +@mock_cloudformation +@mock_route53 +def test_create_stack_hosted_zone_by_id(): + conn = boto.connect_cloudformation() + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 1", + "Parameters": { + }, + "Resources": { + "Bar": { + "Type" : "AWS::Route53::HostedZone", + "Properties" : { + "Name" : "foo.bar.baz", + } + }, + }, + } + dummy_template2 = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack 2", + "Parameters": { + "ZoneId": { "Type": "String" } + }, + "Resources": { + "Foo": { + "Properties": { + "HostedZoneId": {"Ref": "ZoneId"}, + "RecordSets": [] + }, + "Type": "AWS::Route53::RecordSetGroup" + } + }, + } + conn.create_stack( + "test_stack", + template_body=json.dumps(dummy_template), + parameters={}.items() + ) + r53_conn = boto.connect_route53() + zone_id = r53_conn.get_zones()[0].id + conn.create_stack( + "test_stack", + template_body=json.dumps(dummy_template2), + parameters={"ZoneId": zone_id}.items() + ) + + stack = conn.describe_stacks()[0] + assert stack.list_resources() + + @mock_cloudformation def test_creating_stacks_across_regions(): west1_conn = boto.cloudformation.connect_to_region("us-west-1") From 1b6007e2b2b8cf44ec8a3bf799cd51925bd9659d Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:36:25 +1100 Subject: [PATCH 05/15] Correct IAM list_server_certs template that was based off incorrect docs (#836) The documentation for this method is here https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListServerCertificates.html The docs say the return type is this ServerCertificateMetadataList.member.N but the sample response incorrectly include a . I've sent feedback to the AWS docs telling them to fix their stuff but this also needs to be fixed. I haven't checked other templates with tags in them, as they may be prone to this same problem. --- moto/iam/responses.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 223691e1e..6884c2025 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -578,18 +578,16 @@ LIST_SERVER_CERTIFICATES_TEMPLATE = """ {% for certificate in server_certificates %} - - {{ certificate.cert_name }} - {% if certificate.path %} - {{ certificate.path }} - arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} - {% else %} - arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} - {% endif %} - 2010-05-08T01:02:03.004Z - ASCACKCEVSQ6C2EXAMPLE - 2012-05-08T01:02:03.004Z - + {{ certificate.cert_name }} + {% if certificate.path %} + {{ certificate.path }} + arn:aws:iam::123456789012:server-certificate/{{ certificate.path }}/{{ certificate.cert_name }} + {% else %} + arn:aws:iam::123456789012:server-certificate/{{ certificate.cert_name }} + {% endif %} + 2010-05-08T01:02:03.004Z + ASCACKCEVSQ6C2EXAMPLE + 2012-05-08T01:02:03.004Z {% endfor %} From a30ba2b597764d5132c818084933398d9fcf755c Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Sat, 4 Mar 2017 19:37:53 -0800 Subject: [PATCH 06/15] EC2 tags specified in CloudFormation should be applied to the instances (#840) Fixes #839. --- moto/ec2/models.py | 7 +++++-- .../fixtures/vpc_single_instance_in_subnet.py | 4 ++++ .../test_cloudformation_stack_integration.py | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 30769fd7e..35c7bd878 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -336,7 +336,7 @@ class NetworkInterfaceBackend(object): return generic_filter(filters, enis) -class Instance(BotoInstance, TaggedEC2Resource): +class Instance(TaggedEC2Resource, BotoInstance): def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs): super(Instance, self).__init__() self.ec2_backend = ec2_backend @@ -441,7 +441,10 @@ class Instance(BotoInstance, TaggedEC2Resource): key_name=properties.get("KeyName"), private_ip=properties.get('PrivateIpAddress'), ) - return reservation.instances[0] + instance = reservation.instances[0] + for tag in properties.get("Tags", []): + instance.add_tag(tag["Key"], tag["Value"]) + return instance @property def physical_resource_id(self): diff --git a/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py index 1f296cf0c..177da884e 100644 --- a/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py +++ b/tests/test_cloudformation/fixtures/vpc_single_instance_in_subnet.py @@ -236,6 +236,10 @@ template = { "Ref": "AWS::StackId" }, "Key": "Application" + }, + { + "Value": "Bar", + "Key": "Foo" } ], "SecurityGroupIds": [ diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index 4237bee19..f76e02a49 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -676,6 +676,7 @@ def test_vpc_single_instance_in_subnet(): ec2_conn = boto.ec2.connect_to_region("us-west-1") reservation = ec2_conn.get_all_instances()[0] instance = reservation.instances[0] + instance.tags["Foo"].should.equal("Bar") # Check that the EIP is attached the the EC2 instance eip = ec2_conn.get_all_addresses()[0] eip.domain.should.equal('vpc') From 783242b696570c641f748ae4f029f86c09332ffe Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Sat, 4 Mar 2017 19:40:43 -0800 Subject: [PATCH 07/15] Elastic IP PhysicalResourceId should always be its public IP (#841) According to the [CloudFormation `Ref` docs][docs], the `Ref` return value (and physical ID of the resource) for an Elastic IP is its public IP address. [docs]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html --- moto/ec2/models.py | 2 +- .../test_cloudformation_stack_integration.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 35c7bd878..c18f8b390 100755 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2836,7 +2836,7 @@ class ElasticAddress(object): @property def physical_resource_id(self): - return self.allocation_id if self.allocation_id else self.public_ip + return self.public_ip def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index f76e02a49..c168ff723 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -694,7 +694,7 @@ def test_vpc_single_instance_in_subnet(): subnet_resource.physical_resource_id.should.equal(subnet.id) eip_resource = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] - eip_resource.physical_resource_id.should.equal(eip.allocation_id) + eip_resource.physical_resource_id.should.equal(eip.public_ip) @mock_cloudformation() @mock_ec2() @@ -991,7 +991,7 @@ def test_vpc_eip(): stack = conn.describe_stacks()[0] resources = stack.describe_resources() cfn_eip = [resource for resource in resources if resource.resource_type == 'AWS::EC2::EIP'][0] - cfn_eip.physical_resource_id.should.equal(eip.allocation_id) + cfn_eip.physical_resource_id.should.equal(eip.public_ip) @mock_ec2() From a9554924df793fe36d11938208617f8e8a3381c7 Mon Sep 17 00:00:00 2001 From: David Wilcox Date: Sun, 5 Mar 2017 14:48:51 +1100 Subject: [PATCH 08/15] make cloudformation update stack use parameters provided (#843) --- moto/cloudformation/models.py | 8 ++--- moto/cloudformation/parsing.py | 4 ++- moto/cloudformation/responses.py | 6 ++++ .../test_cloudformation_stack_crud.py | 36 +++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index 1f091251b..a9dda8fdc 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -78,10 +78,10 @@ class FakeStack(object): def stack_outputs(self): return self.output_map.values() - def update(self, template, role_arn=None): + def update(self, template, role_arn=None, parameters=None): self._add_stack_event("UPDATE_IN_PROGRESS", resource_status_reason="User Initiated") self.template = template - self.resource_map.update(json.loads(template)) + self.resource_map.update(json.loads(template), parameters) self.output_map = self._create_output_map() self._add_stack_event("UPDATE_COMPLETE") self.status = "UPDATE_COMPLETE" @@ -157,9 +157,9 @@ class CloudFormationBackend(BaseBackend): if stack.name == name_or_stack_id: return stack - def update_stack(self, name, template, role_arn=None): + def update_stack(self, name, template, role_arn=None, parameters=None): stack = self.get_stack(name) - stack.update(template, role_arn) + stack.update(template, role_arn, parameters=parameters) return stack def list_stack_resources(self, stack_name_or_id): diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py index fdc569dc1..06673bd8c 100644 --- a/moto/cloudformation/parsing.py +++ b/moto/cloudformation/parsing.py @@ -374,7 +374,9 @@ class ResourceMap(collections.Mapping): self.tags['aws:cloudformation:logical-id'] = resource ec2_models.ec2_backends[self._region_name].create_tags([self[resource].physical_resource_id], self.tags) - def update(self, template): + def update(self, template, parameters=None): + if parameters: + self.input_parameters = parameters self.load_mapping() self.load_parameters() self.load_conditions() diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index d16b3560c..06d0bbb00 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -138,6 +138,11 @@ class CloudFormationResponse(BaseResponse): stack_body = self.cloudformation_backend.get_stack(stack_name).template else: stack_body = self._get_param('TemplateBody') + parameters = dict([ + (parameter['parameter_key'], parameter['parameter_value']) + for parameter + in self._get_list_prefix("Parameters.member") + ]) stack = self.cloudformation_backend.get_stack(stack_name) if stack.status == 'ROLLBACK_COMPLETE': @@ -147,6 +152,7 @@ class CloudFormationResponse(BaseResponse): name=stack_name, template=stack_body, role_arn=role_arn, + parameters=parameters ) if self.request_json: stack_body = { diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index a2b5a06f5..e145ea283 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -444,6 +444,42 @@ def test_update_stack(): }) +@mock_cloudformation +def test_update_stack_with_parameters(): + dummy_template = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "Stack", + "Resources": { + "VPC": { + "Properties": { + "CidrBlock": {"Ref": "Bar"} + }, + "Type": "AWS::EC2::VPC" + } + }, + "Parameters": { + "Bar": { + "Type": "String" + } + } + } + dummy_template_json = json.dumps(dummy_template) + conn = boto.connect_cloudformation() + conn.create_stack( + "test_stack", + template_body=dummy_template_json, + parameters=[("Bar", "192.168.0.0/16")] + ) + conn.update_stack( + "test_stack", + template_body=dummy_template_json, + parameters=[("Bar", "192.168.0.1/16")] + ) + + stack = conn.describe_stacks()[0] + assert stack.parameters[0].value == "192.168.0.1/16" + + @mock_cloudformation def test_update_stack_when_rolled_back(): conn = boto.connect_cloudformation() From f46a24180f9a2a84b61b05efdb57126cffaca113 Mon Sep 17 00:00:00 2001 From: William Richard Date: Sat, 4 Mar 2017 22:51:01 -0500 Subject: [PATCH 09/15] Cast desired capacity for cloudformation asg to int (#846) Cloudformation passes MaxSize, MinSize and DesiredCapacity as strings, but we want to store them as ints. Also includes tests of this fix, to help avoid regression. --- moto/autoscaling/models.py | 1 + .../test_cloudformation_stack_integration.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py index 53a0f62df..2b5a07c15 100644 --- a/moto/autoscaling/models.py +++ b/moto/autoscaling/models.py @@ -324,6 +324,7 @@ class AutoScalingBackend(BaseBackend): max_size = make_int(max_size) min_size = make_int(min_size) + desired_capacity = make_int(desired_capacity) default_cooldown = make_int(default_cooldown) if health_check_period is None: health_check_period = 300 diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py index c168ff723..f842ffe70 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_integration.py +++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py @@ -535,6 +535,7 @@ def test_autoscaling_group_with_elb(): "LaunchConfigurationName": {"Ref": "my-launch-config"}, "MinSize": "2", "MaxSize": "2", + "DesiredCapacity": "2", "LoadBalancerNames": [{"Ref": "my-elb"}] }, }, @@ -614,6 +615,7 @@ def test_autoscaling_group_update(): "LaunchConfigurationName": {"Ref": "my-launch-config"}, "MinSize": "2", "MaxSize": "2", + "DesiredCapacity": "2" }, }, @@ -638,6 +640,7 @@ def test_autoscaling_group_update(): asg = autoscale_conn.get_all_groups()[0] asg.min_size.should.equal(2) asg.max_size.should.equal(2) + asg.desired_capacity.should.equal(2) asg_template['Resources']['my-as-group']['Properties']['MaxSize'] = 3 asg_template_json = json.dumps(asg_template) @@ -648,6 +651,7 @@ def test_autoscaling_group_update(): asg = autoscale_conn.get_all_groups()[0] asg.min_size.should.equal(2) asg.max_size.should.equal(3) + asg.desired_capacity.should.equal(2) @mock_ec2() From 56f9409ca995b73870316a2ac4ff1d024b3a5cab Mon Sep 17 00:00:00 2001 From: Chris LaRose Date: Sat, 4 Mar 2017 19:53:14 -0800 Subject: [PATCH 10/15] Use request URL to generate SQS queue URLs; fixes #626 (#827) --- moto/sqs/models.py | 5 ++--- moto/sqs/responses.py | 16 ++++++++++------ tests/test_sqs/test_sqs.py | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 13b8c34b6..efe9f9517 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -180,9 +180,8 @@ class Queue(object): result[attribute] = getattr(self, camelcase_to_underscores(attribute)) return result - @property - def url(self): - return "http://sqs.{0}.amazonaws.com/123456789012/{1}".format(self.region, self.name) + def url(self, request_url): + return "{0}://{1}/123456789012/{2}".format(request_url.scheme, request_url.netloc, self.name) @property def messages(self): diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 15c067613..6720a09bd 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from six.moves.urllib.parse import urlparse from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores @@ -57,26 +58,29 @@ class SQSResponse(BaseResponse): return status_code, headers, body def create_queue(self): + request_url = urlparse(self.uri) queue_name = self.querystring.get("QueueName")[0] queue = self.sqs_backend.create_queue(queue_name, visibility_timeout=self.attribute.get('VisibilityTimeout'), wait_time_seconds=self.attribute.get('WaitTimeSeconds')) template = self.response_template(CREATE_QUEUE_RESPONSE) - return template.render(queue=queue) + return template.render(queue=queue, request_url=request_url) def get_queue_url(self): + request_url = urlparse(self.uri) queue_name = self.querystring.get("QueueName")[0] queue = self.sqs_backend.get_queue(queue_name) if queue: template = self.response_template(GET_QUEUE_URL_RESPONSE) - return template.render(queue=queue) + return template.render(queue=queue, request_url=request_url) else: return "", dict(status=404) def list_queues(self): + request_url = urlparse(self.uri) queue_name_prefix = self.querystring.get("QueueNamePrefix", [None])[0] queues = self.sqs_backend.list_queues(queue_name_prefix) template = self.response_template(LIST_QUEUES_RESPONSE) - return template.render(queues=queues) + return template.render(queues=queues, request_url=request_url) def change_message_visibility(self): queue_name = self._get_queue_name() @@ -265,7 +269,7 @@ class SQSResponse(BaseResponse): CREATE_QUEUE_RESPONSE = """ - {{ queue.url }} + {{ queue.url(request_url) }} {{ queue.visibility_timeout }} @@ -275,7 +279,7 @@ CREATE_QUEUE_RESPONSE = """ GET_QUEUE_URL_RESPONSE = """ - {{ queue.url }} + {{ queue.url(request_url) }} 470a6f13-2ed9-4181-ad8a-2fdea142988e @@ -285,7 +289,7 @@ GET_QUEUE_URL_RESPONSE = """ LIST_QUEUES_RESPONSE = """ {% for queue in queues %} - {{ queue.url }} + {{ queue.url(request_url) }} {% endfor %} diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index 32b026a46..2ad5f1af1 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -71,7 +71,7 @@ def test_create_queues_in_multiple_region(): list(west1_conn.list_queues()['QueueUrls']).should.have.length_of(1) list(west2_conn.list_queues()['QueueUrls']).should.have.length_of(1) - west1_conn.list_queues()['QueueUrls'][0].should.equal('http://sqs.us-west-1.amazonaws.com/123456789012/blah') + west1_conn.list_queues()['QueueUrls'][0].should.equal('https://us-west-1.queue.amazonaws.com/123456789012/blah') @mock_sqs @@ -85,7 +85,7 @@ def test_get_queue_with_prefix(): queue = conn.list_queues(QueueNamePrefix="test-")['QueueUrls'] queue.should.have.length_of(1) - queue[0].should.equal("http://sqs.us-west-1.amazonaws.com/123456789012/test-queue") + queue[0].should.equal("https://us-west-1.queue.amazonaws.com/123456789012/test-queue") @mock_sqs From 9b6d3983d2aa4ae41ea67595a8d744c108796b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Sun, 5 Mar 2017 04:56:36 +0100 Subject: [PATCH 11/15] iam: add group policy methods (#849) Implemented mocks for: * get_all_group_policies * list_group_policies (boto3) * get_group_policy * put_group_policy --- moto/iam/models.py | 31 +++++++++++++++++ moto/iam/responses.py | 56 +++++++++++++++++++++++++++++++ tests/test_iam/test_iam_groups.py | 38 +++++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/moto/iam/models.py b/moto/iam/models.py index d27722f33..15a26f663 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -167,6 +167,7 @@ class Group(object): ) self.users = [] + self.policies = {} def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -174,6 +175,24 @@ class Group(object): raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "Arn" ]"') raise UnformattedGetAttTemplateException() + def get_policy(self, policy_name): + try: + policy_json = self.policies[policy_name] + except KeyError: + raise IAMNotFoundException("Policy {0} not found".format(policy_name)) + + return { + 'policy_name': policy_name, + 'policy_document': policy_json, + 'group_name': self.name, + } + + def put_policy(self, policy_name, policy_json): + self.policies[policy_name] = policy_json + + def list_policies(self): + return self.policies.keys() + class User(object): def __init__(self, name, path=None): @@ -573,6 +592,18 @@ class IAMBackend(BaseBackend): return groups + def put_group_policy(self, group_name, policy_name, policy_json): + group = self.get_group(group_name) + group.put_policy(policy_name, policy_json) + + def list_group_policies(self, group_name, marker=None, max_items=None): + group = self.get_group(group_name) + return group.list_policies() + + def get_group_policy(self, group_name, policy_name): + group = self.get_group(group_name) + return group.get_policy(policy_name) + def create_user(self, user_name, path='/'): if user_name in self.users: raise IAMConflictException("EntityAlreadyExists", "User {0} already exists".format(user_name)) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 6884c2025..c23e9bd8e 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -186,6 +186,32 @@ class IamResponse(BaseResponse): template = self.response_template(LIST_GROUPS_FOR_USER_TEMPLATE) return template.render(groups=groups) + def put_group_policy(self): + group_name = self._get_param('GroupName') + policy_name = self._get_param('PolicyName') + policy_document = self._get_param('PolicyDocument') + iam_backend.put_group_policy(group_name, policy_name, policy_document) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name="PutGroupPolicyResponse") + + def list_group_policies(self): + group_name = self._get_param('GroupName') + marker = self._get_param('Marker') + max_items = self._get_param('MaxItems') + policies = iam_backend.list_group_policies(group_name, + marker=marker, max_items=max_items) + template = self.response_template(LIST_GROUP_POLICIES_TEMPLATE) + return template.render(name="ListGroupPoliciesResponse", + policies=policies, + marker=marker) + + def get_group_policy(self): + group_name = self._get_param('GroupName') + policy_name = self._get_param('PolicyName') + policy_result = iam_backend.get_group_policy(group_name, policy_name) + template = self.response_template(GET_GROUP_POLICY_TEMPLATE) + return template.render(name="GetGroupPolicyResponse", **policy_result) + def create_user(self): user_name = self._get_param('UserName') path = self._get_param('Path') @@ -194,6 +220,7 @@ class IamResponse(BaseResponse): template = self.response_template(USER_TEMPLATE) return template.render(action='Create', user=user) + def get_user(self): user_name = self._get_param('UserName') user = iam_backend.get_user(user_name) @@ -699,6 +726,35 @@ LIST_GROUPS_FOR_USER_TEMPLATE = """ """ +LIST_GROUP_POLICIES_TEMPLATE = """ + + {% if marker is none %} + false + {% else %} + true + {{ marker }} + {% endif %} + + {% for policy in policies %} + {{ policy }} + {% endfor %} + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +""" + +GET_GROUP_POLICY_TEMPLATE = """ + + {{ policy_name }} + {{ group_name }} + {{ policy_document }} + + + 7e7cd8bc-99ef-11e1-a4c3-27EXAMPLE804 + +""" USER_TEMPLATE = """<{{ action }}UserResponse> <{{ action }}UserResult> diff --git a/tests/test_iam/test_iam_groups.py b/tests/test_iam/test_iam_groups.py index 412484a70..ccc802283 100644 --- a/tests/test_iam/test_iam_groups.py +++ b/tests/test_iam/test_iam_groups.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import boto +import boto3 import sure # noqa from nose.tools import assert_raises @@ -70,3 +71,40 @@ def test_get_groups_for_user(): groups = conn.get_groups_for_user('my-user')['list_groups_for_user_response']['list_groups_for_user_result']['groups'] groups.should.have.length_of(2) + + +@mock_iam() +def test_put_group_policy(): + conn = boto.connect_iam() + conn.create_group('my-group') + conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}') + + +@mock_iam() +def test_get_group_policy(): + conn = boto.connect_iam() + conn.create_group('my-group') + with assert_raises(BotoServerError): + conn.get_group_policy('my-group', 'my-policy') + + conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}') + policy = conn.get_group_policy('my-group', 'my-policy') + +@mock_iam() +def test_get_all_group_policies(): + conn = boto.connect_iam() + conn.create_group('my-group') + policies = conn.get_all_group_policies('my-group')['list_group_policies_response']['list_group_policies_result']['policy_names'] + assert policies == [] + conn.put_group_policy('my-group', 'my-policy', '{"some": "json"}') + policies = conn.get_all_group_policies('my-group')['list_group_policies_response']['list_group_policies_result']['policy_names'] + assert policies == ['my-policy'] + + +@mock_iam() +def test_list_group_policies(): + conn = boto3.client('iam') + conn.create_group(GroupName='my-group') + policies = conn.list_group_policies(GroupName='my-group')['PolicyNames'].should.be.empty + conn.put_group_policy(GroupName='my-group', PolicyName='my-policy', PolicyDocument='{"some": "json"}') + policies = conn.list_group_policies(GroupName='my-group')['PolicyNames'].should.equal(['my-policy']) From f6465df63077ad2b738c0dd94baaa3cca1a4f2c9 Mon Sep 17 00:00:00 2001 From: Andrew Garrett <2rs2ts@users.noreply.github.com> Date: Sat, 4 Mar 2017 20:00:25 -0800 Subject: [PATCH 12/15] Return CF Stack events in reverse chronological order (#853) This is how the AWS API works: http://boto3.readthedocs.io/en/latest/reference/services/cloudformation.html#CloudFormation.Client.describe_stack_events --- moto/cloudformation/responses.py | 2 +- .../test_cloudformation_stack_crud.py | 11 ++++++++--- .../test_cloudformation_stack_crud_boto3.py | 11 ++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index 06d0bbb00..695118319 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -292,7 +292,7 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """ DESCRIBE_STACK_EVENTS_RESPONSE = """ - {% for event in stack.events %} + {% for event in stack.events[::-1] %} {{ event.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ') }} {{ event.resource_status }} diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index e145ea283..7eb563c42 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -509,10 +509,15 @@ def test_describe_stack_events_shows_create_update_and_delete(): events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") # testing ordering of stack events without assuming resource events will not exist + # the AWS API returns events in reverse chronological order stack_events_to_look_for = iter([ - ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), - ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), - ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) + ("DELETE_COMPLETE", None), + ("DELETE_IN_PROGRESS", "User Initiated"), + ("UPDATE_COMPLETE", None), + ("UPDATE_IN_PROGRESS", "User Initiated"), + ("CREATE_COMPLETE", None), + ("CREATE_IN_PROGRESS", "User Initiated"), + ]) try: for event in events: event.stack_id.should.equal(stack_id) diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 97c3e864a..98ed213e5 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -345,10 +345,15 @@ def test_stack_events(): events[-1].resource_type.should.equal("AWS::CloudFormation::Stack") # testing ordering of stack events without assuming resource events will not exist + # the AWS API returns events in reverse chronological order stack_events_to_look_for = iter([ - ("CREATE_IN_PROGRESS", "User Initiated"), ("CREATE_COMPLETE", None), - ("UPDATE_IN_PROGRESS", "User Initiated"), ("UPDATE_COMPLETE", None), - ("DELETE_IN_PROGRESS", "User Initiated"), ("DELETE_COMPLETE", None)]) + ("DELETE_COMPLETE", None), + ("DELETE_IN_PROGRESS", "User Initiated"), + ("UPDATE_COMPLETE", None), + ("UPDATE_IN_PROGRESS", "User Initiated"), + ("CREATE_COMPLETE", None), + ("CREATE_IN_PROGRESS", "User Initiated"), + ]) try: for event in events: event.stack_id.should.equal(stack.stack_id) From e7ea6b350c848c3ecbd6990db4c224250a0a7511 Mon Sep 17 00:00:00 2001 From: Andrew Garrett <2rs2ts@users.noreply.github.com> Date: Sat, 4 Mar 2017 20:01:50 -0800 Subject: [PATCH 13/15] Fix lambda stdout/stderr mocking (#851) Originally, the code was setting sys.stdout and sys.stderr back to the original, official forms, but this breaks idioms like mocking stdout to capture printing output for tests. So instead, we will reset sys.stdout and sys.stderr to what they were before running the lambda function, so that in case someone is mocking stdout or stderr, their tests won't break. --- moto/awslambda/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index 069717ca4..c0593bcde 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -135,6 +135,8 @@ class LambdaFunction(object): print("Exception %s", ex) try: + original_stdout = sys.stdout + original_stderr = sys.stderr codeOut = StringIO() codeErr = StringIO() sys.stdout = codeOut @@ -150,8 +152,8 @@ class LambdaFunction(object): finally: codeErr.close() codeOut.close() - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ + sys.stdout = original_stdout + sys.stderr = original_stderr return self.convert(result) def invoke(self, request, headers): From e7735c3ee1661ad4269ac9862c61600714668ff0 Mon Sep 17 00:00:00 2001 From: Andrew Garrett <2rs2ts@users.noreply.github.com> Date: Sat, 4 Mar 2017 20:12:55 -0800 Subject: [PATCH 14/15] Add event IDs to CF Stack events (#852) So that events can be uniquely identified. I tried to match the format documented here: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-listing-event-history.html --- moto/cloudformation/models.py | 2 ++ tests/test_cloudformation/test_cloudformation_stack_crud.py | 1 + .../test_cloudformation/test_cloudformation_stack_crud_boto3.py | 1 + 3 files changed, 4 insertions(+) diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py index a9dda8fdc..e9493b2b6 100644 --- a/moto/cloudformation/models.py +++ b/moto/cloudformation/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from datetime import datetime import json +import uuid import boto.cloudformation from moto.core import BaseBackend @@ -105,6 +106,7 @@ class FakeEvent(object): self.resource_status_reason = resource_status_reason self.resource_properties = resource_properties self.timestamp = datetime.utcnow() + self.event_id = uuid.uuid4() class CloudFormationBackend(BaseBackend): diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud.py b/tests/test_cloudformation/test_cloudformation_stack_crud.py index 7eb563c42..1a2d16e94 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud.py @@ -522,6 +522,7 @@ def test_describe_stack_events_shows_create_update_and_delete(): for event in events: event.stack_id.should.equal(stack_id) event.stack_name.should.equal("test_stack") + event.event_id.should.match(r"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}") if event.resource_type == "AWS::CloudFormation::Stack": event.logical_resource_id.should.equal("test_stack") diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py index 98ed213e5..112b8bd04 100644 --- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py +++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py @@ -358,6 +358,7 @@ def test_stack_events(): for event in events: event.stack_id.should.equal(stack.stack_id) event.stack_name.should.equal("test_stack") + event.event_id.should.match(r"[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}") if event.resource_type == "AWS::CloudFormation::Stack": event.logical_resource_id.should.equal("test_stack") From 0393c384adec93c8808dbb6503dae5936e3f26e8 Mon Sep 17 00:00:00 2001 From: Matt Chamberlin Date: Sat, 4 Mar 2017 20:17:18 -0800 Subject: [PATCH 15/15] fix etag metadata field name in key response dict (etag --> ETag) (#855) --- moto/s3/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index c41ff3901..c60c49b72 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -90,7 +90,7 @@ class FakeKey(object): @property def response_dict(self): r = { - 'etag': self.etag, + 'Etag': self.etag, 'last-modified': self.last_modified_RFC1123, } if self._storage_class != 'STANDARD':