Repository: getmoto/moto

Issue:
Add support  for organizations API to tag_resource and list_tag_for_resources
Hi
Thank you for creating this amazing library.

Currently I'm working creating tools for managing aws organizations and account(organizations api), unfortunately not all functions is covered yet [IMPLEMENTATION_COVERAGE](https://github.com/spulec/moto/blob/master/IMPLEMENTATION_COVERAGE.md),
I would like to mockout these functions:
- `tag_resource`
- `list_tag_for_resources`

I'm currently working on this issue in my local  and will create PR for this feature, might need your help and guidance.


Discussion:
Cool, thanks for opening. Setting this as a feature request.

Resolving patch:
diff --git a/moto/organizations/exceptions.py b/moto/organizations/exceptions.py
new file mode 100644
--- /dev/null
+++ b/moto/organizations/exceptions.py
@@ -0,0 +1,12 @@
+from __future__ import unicode_literals
+from moto.core.exceptions import JsonRESTError
+
+
+class InvalidInputException(JsonRESTError):
+    code = 400
+
+    def __init__(self):
+        super(InvalidInputException, self).__init__(
+            "InvalidInputException",
+            "You provided a value that does not match the required pattern.",
+        )
diff --git a/moto/organizations/models.py b/moto/organizations/models.py
--- a/moto/organizations/models.py
+++ b/moto/organizations/models.py
@@ -8,6 +8,7 @@
 from moto.core.exceptions import RESTError
 from moto.core.utils import unix_time
 from moto.organizations import utils
+from moto.organizations.exceptions import InvalidInputException
 
 
 class FakeOrganization(BaseModel):
@@ -57,6 +58,7 @@ def __init__(self, organization, **kwargs):
         self.joined_method = "CREATED"
         self.parent_id = organization.root_id
         self.attached_policies = []
+        self.tags = {}
 
     @property
     def arn(self):
@@ -442,5 +444,32 @@ def list_targets_for_policy(self, **kwargs):
         ]
         return dict(Targets=objects)
 
+    def tag_resource(self, **kwargs):
+        account = next((a for a in self.accounts if a.id == kwargs["ResourceId"]), None)
+
+        if account is None:
+            raise InvalidInputException
+
+        new_tags = {tag["Key"]: tag["Value"] for tag in kwargs["Tags"]}
+        account.tags.update(new_tags)
+
+    def list_tags_for_resource(self, **kwargs):
+        account = next((a for a in self.accounts if a.id == kwargs["ResourceId"]), None)
+
+        if account is None:
+            raise InvalidInputException
+
+        tags = [{"Key": key, "Value": value} for key, value in account.tags.items()]
+        return dict(Tags=tags)
+
+    def untag_resource(self, **kwargs):
+        account = next((a for a in self.accounts if a.id == kwargs["ResourceId"]), None)
+
+        if account is None:
+            raise InvalidInputException
+
+        for key in kwargs["TagKeys"]:
+            account.tags.pop(key, None)
+
 
 organizations_backend = OrganizationsBackend()
diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py
--- a/moto/organizations/responses.py
+++ b/moto/organizations/responses.py
@@ -119,3 +119,18 @@ def list_targets_for_policy(self):
         return json.dumps(
             self.organizations_backend.list_targets_for_policy(**self.request_params)
         )
+
+    def tag_resource(self):
+        return json.dumps(
+            self.organizations_backend.tag_resource(**self.request_params)
+        )
+
+    def list_tags_for_resource(self):
+        return json.dumps(
+            self.organizations_backend.list_tags_for_resource(**self.request_params)
+        )
+
+    def untag_resource(self):
+        return json.dumps(
+            self.organizations_backend.untag_resource(**self.request_params)
+        )


Test patch:
diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py
--- a/tests/test_organizations/test_organizations_boto3.py
+++ b/tests/test_organizations/test_organizations_boto3.py
@@ -3,6 +3,7 @@
 import boto3
 import json
 import six
+import sure  # noqa
 from botocore.exceptions import ClientError
 from nose.tools import assert_raises
 
@@ -605,3 +606,110 @@ def test_list_targets_for_policy_exception():
     ex.operation_name.should.equal("ListTargetsForPolicy")
     ex.response["Error"]["Code"].should.equal("400")
     ex.response["Error"]["Message"].should.contain("InvalidInputException")
+
+
+@mock_organizations
+def test_tag_resource():
+    client = boto3.client("organizations", region_name="us-east-1")
+    client.create_organization(FeatureSet="ALL")
+    account_id = client.create_account(AccountName=mockname, Email=mockemail)[
+        "CreateAccountStatus"
+    ]["AccountId"]
+
+    client.tag_resource(ResourceId=account_id, Tags=[{"Key": "key", "Value": "value"}])
+
+    response = client.list_tags_for_resource(ResourceId=account_id)
+    response["Tags"].should.equal([{"Key": "key", "Value": "value"}])
+
+    # adding a tag with an existing key, will update the value
+    client.tag_resource(
+        ResourceId=account_id, Tags=[{"Key": "key", "Value": "new-value"}]
+    )
+
+    response = client.list_tags_for_resource(ResourceId=account_id)
+    response["Tags"].should.equal([{"Key": "key", "Value": "new-value"}])
+
+
+@mock_organizations
+def test_tag_resource_errors():
+    client = boto3.client("organizations", region_name="us-east-1")
+    client.create_organization(FeatureSet="ALL")
+
+    with assert_raises(ClientError) as e:
+        client.tag_resource(
+            ResourceId="000000000000", Tags=[{"Key": "key", "Value": "value"},]
+        )
+    ex = e.exception
+    ex.operation_name.should.equal("TagResource")
+    ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    ex.response["Error"]["Code"].should.contain("InvalidInputException")
+    ex.response["Error"]["Message"].should.equal(
+        "You provided a value that does not match the required pattern."
+    )
+
+
+@mock_organizations
+def test_list_tags_for_resource():
+    client = boto3.client("organizations", region_name="us-east-1")
+    client.create_organization(FeatureSet="ALL")
+    account_id = client.create_account(AccountName=mockname, Email=mockemail)[
+        "CreateAccountStatus"
+    ]["AccountId"]
+    client.tag_resource(ResourceId=account_id, Tags=[{"Key": "key", "Value": "value"}])
+
+    response = client.list_tags_for_resource(ResourceId=account_id)
+
+    response["Tags"].should.equal([{"Key": "key", "Value": "value"}])
+
+
+@mock_organizations
+def test_list_tags_for_resource_errors():
+    client = boto3.client("organizations", region_name="us-east-1")
+    client.create_organization(FeatureSet="ALL")
+
+    with assert_raises(ClientError) as e:
+        client.list_tags_for_resource(ResourceId="000000000000")
+    ex = e.exception
+    ex.operation_name.should.equal("ListTagsForResource")
+    ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    ex.response["Error"]["Code"].should.contain("InvalidInputException")
+    ex.response["Error"]["Message"].should.equal(
+        "You provided a value that does not match the required pattern."
+    )
+
+
+@mock_organizations
+def test_untag_resource():
+    client = boto3.client("organizations", region_name="us-east-1")
+    client.create_organization(FeatureSet="ALL")
+    account_id = client.create_account(AccountName=mockname, Email=mockemail)[
+        "CreateAccountStatus"
+    ]["AccountId"]
+    client.tag_resource(ResourceId=account_id, Tags=[{"Key": "key", "Value": "value"}])
+    response = client.list_tags_for_resource(ResourceId=account_id)
+    response["Tags"].should.equal([{"Key": "key", "Value": "value"}])
+
+    # removing a non existing tag should not raise any error
+    client.untag_resource(ResourceId=account_id, TagKeys=["not-existing"])
+    response = client.list_tags_for_resource(ResourceId=account_id)
+    response["Tags"].should.equal([{"Key": "key", "Value": "value"}])
+
+    client.untag_resource(ResourceId=account_id, TagKeys=["key"])
+    response = client.list_tags_for_resource(ResourceId=account_id)
+    response["Tags"].should.have.length_of(0)
+
+
+@mock_organizations
+def test_untag_resource_errors():
+    client = boto3.client("organizations", region_name="us-east-1")
+    client.create_organization(FeatureSet="ALL")
+
+    with assert_raises(ClientError) as e:
+        client.untag_resource(ResourceId="000000000000", TagKeys=["key"])
+    ex = e.exception
+    ex.operation_name.should.equal("UntagResource")
+    ex.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    ex.response["Error"]["Code"].should.contain("InvalidInputException")
+    ex.response["Error"]["Message"].should.equal(
+        "You provided a value that does not match the required pattern."
+    )