Committed by
GitHub
Merge pull request #70 from coopernurse/role-delete-idempotent
Role delete idempotent
Showing
3 changed files
with
106 additions
and
25 deletions
... | @@ -38,9 +38,11 @@ class Role(object): | ... | @@ -38,9 +38,11 @@ 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 = iam_client | ||
45 | + if not iam_client: | ||
44 | self._iam_client = kappa.awsclient.create_client( | 46 | self._iam_client = kappa.awsclient.create_client( |
45 | 'iam', context.session) | 47 | 'iam', context.session) |
46 | self._arn = None | 48 | self._arn = None |
... | @@ -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']: | ... | ... |
tests/unit/test_role.py
0 → 100644
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) |
-
Please register or login to post a comment