Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GCP IAM Role #53490

Merged
merged 8 commits into from
Mar 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 12 additions & 3 deletions lib/ansible/module_utils/gcp_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ansible.module_utils._text import to_text
import ast
import os
import json


def navigate_hash(source, path, default=None):
Expand Down Expand Up @@ -143,7 +144,8 @@ def _validate(self):
msg="Service Account Email only works with Machine Account-based authentication"
)

if self.module.params.get('service_account_file') is not None and self.module.params['auth_kind'] != 'serviceaccount':
if (self.module.params.get('service_account_file') is not None or
self.module.params.get('service_account_contents') is not None) and self.module.params['auth_kind'] != 'serviceaccount':
self.module.fail_json(
msg="Service Account File only works with Service Account-based authentication"
)
Expand All @@ -153,9 +155,12 @@ def _credentials(self):
if cred_type == 'application':
credentials, project_id = google.auth.default(scopes=self.module.params['scopes'])
return credentials
elif cred_type == 'serviceaccount':
elif cred_type == 'serviceaccount' and self.module.params.get('service_account_file'):
path = os.path.realpath(os.path.expanduser(self.module.params['service_account_file']))
return service_account.Credentials.from_service_account_file(path).with_scopes(self.module.params['scopes'])
elif cred_type == 'serviceaccount' and self.module.params.get('service_account_contents'):
cred = json.loads(self.module.params.get('service_account_contents'))
return service_account.Credentials.from_service_account_info(cred).with_scopes(self.module.params['scopes'])
elif cred_type == 'machineaccount':
return google.auth.compute_engine.Credentials(
self.module.params['service_account_email'])
Expand Down Expand Up @@ -199,6 +204,10 @@ def __init__(self, *args, **kwargs):
required=False,
fallback=(env_fallback, ['GCP_SERVICE_ACCOUNT_FILE']),
type='path'),
service_account_contents=dict(
required=False,
fallback=(env_fallback, ['GCP_SERVICE_ACCOUNT_CONTENTS']),
type='str'),
scopes=dict(
required=False,
fallback=(env_fallback, ['GCP_SCOPES']),
Expand All @@ -211,7 +220,7 @@ def __init__(self, *args, **kwargs):
mutual = kwargs['mutually_exclusive']

kwargs['mutually_exclusive'] = mutual.append(
['service_account_email', 'service_account_file']
['service_account_email', 'service_account_file', 'service_account_contents']
)

AnsibleModule.__init__(self, *args, **kwargs)
Expand Down
316 changes: 316 additions & 0 deletions lib/ansible/modules/cloud/google/gcp_iam_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,316 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2017 Google
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# ----------------------------------------------------------------------------
#
# *** AUTO GENERATED CODE *** AUTO GENERATED CODE ***
#
# ----------------------------------------------------------------------------
#
# This file is automatically generated by Magic Modules and manual
# changes will be clobbered when the file is regenerated.
#
# Please read more about how to change this file at
# https://www.github.com/GoogleCloudPlatform/magic-modules
#
# ----------------------------------------------------------------------------

from __future__ import absolute_import, division, print_function

__metaclass__ = type

################################################################################
# Documentation
################################################################################

ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ["preview"], 'supported_by': 'community'}

DOCUMENTATION = '''
---
module: gcp_iam_role
description:
- A role in the Identity and Access Management API .
short_description: Creates a GCP Role
version_added: 2.8
author: Google Inc. (@googlecloudplatform)
requirements:
- python >= 2.6
- requests >= 2.18.4
- google-auth >= 1.3.0
options:
state:
description:
- Whether the given object should exist in GCP
choices:
- present
- absent
default: present
name:
description:
- The name of the role.
required: true
title:
description:
- A human-readable title for the role. Typically this is limited to 100 UTF-8
bytes.
required: false
description:
description:
- Human-readable description for the role.
required: false
included_permissions:
description:
- Names of permissions this role grants when bound in an IAM policy.
required: false
stage:
description:
- The current launch stage of the role.
required: false
choices:
- ALPHA
- BETA
- GA
- DEPRECATED
- DISABLED
- EAP
extends_documentation_fragment: gcp
'''

EXAMPLES = '''
- name: create a role
gcp_iam_role:
name: myCustomRole2
title: My Custom Role
description: My custom role description
included_permissions:
- iam.roles.list
- iam.roles.create
- iam.roles.delete
project: test_project
auth_kind: serviceaccount
service_account_file: "/tmp/auth.pem"
state: present
'''

RETURN = '''
name:
description:
- The name of the role.
returned: success
type: str
title:
description:
- A human-readable title for the role. Typically this is limited to 100 UTF-8 bytes.
returned: success
type: str
description:
description:
- Human-readable description for the role.
returned: success
type: str
includedPermissions:
description:
- Names of permissions this role grants when bound in an IAM policy.
returned: success
type: list
stage:
description:
- The current launch stage of the role.
returned: success
type: str
deleted:
description:
- The current deleted state of the role.
returned: success
type: bool
'''

################################################################################
# Imports
################################################################################

from ansible.module_utils.gcp_utils import navigate_hash, GcpSession, GcpModule, GcpRequest, replace_resource_dict
import json

################################################################################
# Main
################################################################################


def main():
"""Main function"""

module = GcpModule(
argument_spec=dict(
state=dict(default='present', choices=['present', 'absent'], type='str'),
name=dict(required=True, type='str'),
title=dict(type='str'),
description=dict(type='str'),
included_permissions=dict(type='list', elements='str'),
stage=dict(type='str', choices=['ALPHA', 'BETA', 'GA', 'DEPRECATED', 'DISABLED', 'EAP']),
)
)

if not module.params['scopes']:
module.params['scopes'] = ['https://www.googleapis.com/auth/iam']

state = module.params['state']

fetch = fetch_resource(module, self_link(module))
changed = False

if fetch:
if state == 'present':
if is_different(module, fetch):
update(module, self_link(module), fetch)
fetch = fetch_resource(module, self_link(module))
changed = True
else:
delete(module, self_link(module))
fetch = {}
changed = True
else:
if state == 'present':
fetch = create(module, collection(module))
changed = True
else:
fetch = {}

fetch.update({'changed': changed})

module.exit_json(**fetch)


def create(module, link):
auth = GcpSession(module, 'iam')
return return_if_object(module, auth.post(link, resource_to_create(module)))


def update(module, link, fetch):
auth = GcpSession(module, 'iam')
params = {'updateMask': updateMask(resource_to_request(module), response_to_hash(module, fetch))}
request = resource_to_request(module)
del request['name']
return return_if_object(module, auth.put(link, request, params=params))


def updateMask(request, response):
update_mask = []
if request.get('name') != response.get('name'):
update_mask.append('name')
if request.get('title') != response.get('title'):
update_mask.append('title')
if request.get('description') != response.get('description'):
update_mask.append('description')
if request.get('includedPermissions') != response.get('includedPermissions'):
update_mask.append('includedPermissions')
if request.get('stage') != response.get('stage'):
update_mask.append('stage')
return ','.join(update_mask)


def delete(module, link):
auth = GcpSession(module, 'iam')
return return_if_object(module, auth.delete(link))


def resource_to_request(module):
request = {
u'name': module.params.get('name'),
u'title': module.params.get('title'),
u'description': module.params.get('description'),
u'includedPermissions': module.params.get('included_permissions'),
u'stage': module.params.get('stage'),
}
return_vals = {}
for k, v in request.items():
if v or v is False:
return_vals[k] = v

return return_vals


def fetch_resource(module, link, allow_not_found=True):
auth = GcpSession(module, 'iam')
return return_if_object(module, auth.get(link), allow_not_found)


def self_link(module):
return "https://iam.googleapis.com/v1/projects/{project}/roles/{name}".format(**module.params)


def collection(module):
return "https://iam.googleapis.com/v1/projects/{project}/roles".format(**module.params)


def return_if_object(module, response, allow_not_found=False):
# If not found, return nothing.
if allow_not_found and response.status_code == 404:
return None

# If no content, return nothing.
if response.status_code == 204:
return None

try:
module.raise_for_status(response)
result = response.json()
except getattr(json.decoder, 'JSONDecodeError', ValueError):
module.fail_json(msg="Invalid JSON response with error: %s" % response.text)

result = decode_response(result, module)

if navigate_hash(result, ['error', 'errors']):
module.fail_json(msg=navigate_hash(result, ['error', 'errors']))

return result


def is_different(module, response):
request = resource_to_request(module)
response = response_to_hash(module, response)
request = decode_response(request, module)

# Remove all output-only from response.
response_vals = {}
for k, v in response.items():
if k in request:
response_vals[k] = v

request_vals = {}
for k, v in request.items():
if k in response:
request_vals[k] = v

return GcpRequest(request_vals) != GcpRequest(response_vals)


# Remove unnecessary properties from the response.
# This is for doing comparisons with Ansible's current parameters.
def response_to_hash(module, response):
return {
u'name': response.get(u'name'),
u'title': response.get(u'title'),
u'description': response.get(u'description'),
u'includedPermissions': response.get(u'includedPermissions'),
u'stage': response.get(u'stage'),
u'deleted': response.get(u'deleted'),
}


def resource_to_create(module):
role = resource_to_request(module)
del role['name']
return {'roleId': module.params['name'], 'role': role}


def decode_response(response, module):
if 'name' in response:
response['name'] = response['name'].split('/')[-1]
return response


if __name__ == '__main__':
main()
8 changes: 8 additions & 0 deletions lib/ansible/plugins/doc_fragments/gcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ class ModuleDocFragment(object):
type: str
required: true
choices: [ application, machineaccount, serviceaccount ]
service_account_contents:
description:
- A string representing the contents of a Service Account JSON file.
- This should not be passed in as a dictionary, but a string
that has the exact contents of a service account json file (valid JSON)
type: str
service_account_file:
description:
- The path of a Service Account JSON file if serviceaccount is selected as type.
Expand All @@ -36,6 +42,8 @@ class ModuleDocFragment(object):
C(GCP_SERVICE_ACCOUNT_FILE) env variable.
- For authentication, you can set service_account_email using the
C(GCP_SERVICE_ACCOUNT_EMAIL) env variable.
- For authentication, you can set service_account_contents using the
C(GCP_SERVICE_ACCOUNT_CONTENTS) env variable.
- For authentication, you can set auth_kind using the C(GCP_AUTH_KIND) env
variable.
- For authentication, you can set scopes using the C(GCP_SCOPES) env variable.
Expand Down
2 changes: 2 additions & 0 deletions test/integration/targets/gcp_iam_role/aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cloud/gcp
unsupported
3 changes: 3 additions & 0 deletions test/integration/targets/gcp_iam_role/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
# defaults file
resource_name: '{{resource_prefix}}'
Empty file.