Jose Diaz-Gonzalez
Committed by GitHub

Merge pull request #70 from coopernurse/role-delete-idempotent

Role delete idempotent
...@@ -38,11 +38,13 @@ class Role(object): ...@@ -38,11 +38,13 @@ class Role(object):
38 38
39 Path = '/kappa/' 39 Path = '/kappa/'
40 40
41 - def __init__(self, context, config): 41 + def __init__(self, context, config, iam_client=None):
42 self._context = context 42 self._context = context
43 self._config = config 43 self._config = config
44 - self._iam_client = kappa.awsclient.create_client( 44 + self._iam_client = iam_client
45 - 'iam', context.session) 45 + if not iam_client:
46 + self._iam_client = kappa.awsclient.create_client(
47 + 'iam', context.session)
46 self._arn = None 48 self._arn = None
47 49
48 @property 50 @property
...@@ -52,28 +54,28 @@ class Role(object): ...@@ -52,28 +54,28 @@ class Role(object):
52 @property 54 @property
53 def arn(self): 55 def arn(self):
54 if self._arn is None: 56 if self._arn is None:
55 - try: 57 + role = self._get_role()
56 - response = self._iam_client.call( 58 + if role:
57 - 'get_role', RoleName=self.name) 59 + self._arn = role['Arn']
58 - LOG.debug(response) 60 + else:
59 - self._arn = response['Role']['Arn']
60 - except Exception:
61 LOG.debug('Unable to find ARN for role: %s', self.name) 61 LOG.debug('Unable to find ARN for role: %s', self.name)
62 return self._arn 62 return self._arn
63 63
64 - def _find_all_roles(self): 64 + def _get_role(self):
65 try: 65 try:
66 - response = self._iam_client.call('list_roles') 66 + response = self._iam_client.call('get_role', RoleName=self.name)
67 + if response and 'Role' in response:
68 + response = response['Role']
69 + return response
70 + except ClientError as e:
71 + if e.response['Error']['Code'] != 'NoSuchEntity':
72 + LOG.exception('Error getting role')
67 except Exception: 73 except Exception:
68 - LOG.exception('Error listing roles') 74 + LOG.exception('Error getting role')
69 - response = {} 75 + return None
70 - return response.get('Roles', list())
71 76
72 def exists(self): 77 def exists(self):
73 - for role in self._find_all_roles(): 78 + return self._get_role() is not None
74 - if role['RoleName'] == self.name:
75 - return role
76 - return None
77 79
78 def create(self): 80 def create(self):
79 LOG.info('creating role %s', self.name) 81 LOG.info('creating role %s', self.name)
...@@ -99,6 +101,10 @@ class Role(object): ...@@ -99,6 +101,10 @@ class Role(object):
99 101
100 def delete(self): 102 def delete(self):
101 response = None 103 response = None
104 + if not self.exists():
105 + LOG.debug('role %s does not exist - skipping delete', self.name)
106 + return response
107 +
102 LOG.debug('deleting role %s', self.name) 108 LOG.debug('deleting role %s', self.name)
103 try: 109 try:
104 LOG.debug('First detach the policy from the role') 110 LOG.debug('First detach the policy from the role')
...@@ -117,11 +123,7 @@ class Role(object): ...@@ -117,11 +123,7 @@ class Role(object):
117 123
118 def status(self): 124 def status(self):
119 LOG.debug('getting status for role %s', self.name) 125 LOG.debug('getting status for role %s', self.name)
120 - try: 126 + role = self._get_role()
121 - response = self._iam_client.call( 127 + if not role:
122 - 'get_role', RoleName=self.name)
123 - LOG.debug(response)
124 - except ClientError:
125 LOG.debug('role %s not found', self.name) 128 LOG.debug('role %s not found', self.name)
126 - response = None 129 + return role
127 - return response
......
...@@ -108,8 +108,8 @@ def status(ctx): ...@@ -108,8 +108,8 @@ def status(ctx):
108 click.echo(click.style('Role', bold=True)) 108 click.echo(click.style('Role', bold=True))
109 if status['role']: 109 if status['role']:
110 line = ' {} ({})'.format( 110 line = ' {} ({})'.format(
111 - status['role']['Role']['RoleName'], 111 + status['role']['RoleName'],
112 - status['role']['Role']['Arn']) 112 + status['role']['Arn'])
113 click.echo(click.style(line, fg='green')) 113 click.echo(click.style(line, fg='green'))
114 click.echo(click.style('Function', bold=True)) 114 click.echo(click.style('Function', bold=True))
115 if status['function']: 115 if status['function']:
......
1 +# -*- coding: utf-8 -*-
2 +# Copyright (c) 2015 Mitch Garnaat http://garnaat.org/
3 +#
4 +# Licensed under the Apache License, Version 2.0 (the "License"). You
5 +# may not use this file except in compliance with the License. A copy of
6 +# the License is located at
7 +#
8 +# http://aws.amazon.com/apache2.0/
9 +#
10 +# or in the "license" file accompanying this file. This file is
11 +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12 +# ANY KIND, either express or implied. See the License for the specific
13 +# language governing permissions and limitations under the License.
14 +
15 +import unittest
16 +import random
17 +import string
18 +from mock import Mock, call
19 +
20 +from kappa.role import Role
21 +
22 +
23 +def randomword(length):
24 + return ''.join(random.choice(string.printable) for i in range(length))
25 +
26 +
27 +class TestRole(unittest.TestCase):
28 +
29 + def setUp(self):
30 + self.iam_client = Mock()
31 + policy = type('Policy', (object,), {
32 + 'arn': None
33 + })
34 + self.context = type('Context', (object,), {
35 + 'name': randomword(10),
36 + 'environment': randomword(10),
37 + 'policy': policy
38 + })
39 + self.role_record = {
40 + 'RoleName': '%s_%s' % (self.context.name,
41 + self.context.environment),
42 + 'Arn': randomword(10)
43 + }
44 + self.role = Role(self.context, None, iam_client=self.iam_client)
45 +
46 + def _expect_get_role(self):
47 + get_role_resp = {'Role': self.role_record}
48 + self.iam_client.configure_mock(**{
49 + 'call.return_value': get_role_resp
50 + })
51 + return get_role_resp
52 +
53 + def test_delete_no_ops_if_role_not_found(self):
54 + self.iam_client.configure_mock(**{
55 + 'call.return_value': None
56 + })
57 + self.assertEquals(None, self.role.delete())
58 + self.assertEquals(1, self.iam_client.call.call_count)
59 +
60 + def test_delete_when_role_exists(self):
61 + get_role_resp = self._expect_get_role()
62 + self.assertEquals(get_role_resp, self.role.delete())
63 + self.iam_client.call.assert_has_calls([call('get_role',
64 + RoleName=self.role.name),
65 + call('delete_role',
66 + RoleName=self.role.name)])
67 +
68 + def test_delete_policy_when_context_arn_exists(self):
69 + self.context.policy.arn = randomword(10)
70 + get_role_resp = self._expect_get_role()
71 + self.assertEquals(get_role_resp, self.role.delete())
72 + calls = [call('get_role',
73 + RoleName=self.role.name),
74 + call('detach_role_policy',
75 + RoleName=self.role.name,
76 + PolicyArn=self.context.policy.arn),
77 + call('delete_role',
78 + RoleName=self.role.name)]
79 + self.iam_client.call.assert_has_calls(calls)