Added gitlab_runner module (#47727)

* Added gitlab_runner module
This commit is contained in:
Samy Coenen 2018-11-06 13:09:17 +01:00 committed by John R Barker
parent d2c7665be9
commit 1104387164

View file

@ -0,0 +1,444 @@
#!/usr/bin/python
# Copyright: (c) 2018, Samy Coenen <samy.coenen@nubera.be>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: gitlab_runner
short_description: Create, modify and delete GitLab Runners.
description:
- Register, update and delete runners with the GitLab API.
- All operations are performed using the GitLab API v4.
- For details, consult the full API documentation at U(https://docs.gitlab.com/ee/api/runners.html).
- A valid private API token is required for all operations. You can create as many tokens as you like using the GitLab web interface at
U(https://$GITLAB_URL/profile/personal_access_tokens).
- A valid registration token is required for registering a new runner.
To create shared runners, you need to ask your administrator to give you this token.
It can be found at U(https://$GITLAB_URL/admin/runners/).
notes:
- Instead of the private_token parameter, the GITLAB_PRIVATE_TOKEN environment variable can be used.
- To create a new runner at least the C(private_token), C(registration_token), C(name) and C(url) options are required.
- Runners need to have unique names.
version_added: 2.8
author: "Samy Coenen (SamyCoenen)"
options:
private_token:
description:
- Your private token to interact with the GitLab API.
required: True
type: str
name:
description:
- The unique name of the runner.
required: True
type: str
api_timeout:
description:
- The maximum time that a request will be attempted to the GitLab API.
required: False
default: 30
type: int
state:
description:
- Make sure that the runner with the same name exists with the same configuration or delete the runner with the same name.
required: False
default: "present"
choices: ["present", "absent"]
type: str
registration_token:
description:
- The registration token is used to register new runners.
required: True
type: str
url:
description:
- The GitLab URL including the API v4 path and http or https.
required: False
default: "https://gitlab.com/api/v4/"
type: str
active:
description:
- Define if the runners is immediately active after creation.
required: False
default: True
choices: [True, False]
type: bool
locked:
description:
- Determines if the runner is locked or not.
required: False
default: False
choices: [true, false]
type: bool
access_level:
description:
- Determines if a runner can pick up jobs from protected branches.
required: False
default: "ref_protected"
choices: ["ref_protected", "not_protected"]
type: str
maximum_timeout:
description:
- The maximum timeout that a runner has to pick up a specific job.
required: False
default: 3600
type: int
run_untagged:
description:
- Run untagged jobs or not.
required: False
default: True
type: bool
tag_list:
description: The tags that apply to the runner.
required: False
default: ["docker"]
type: list
'''
EXAMPLES = '''
# Register a new runner (if it does not exist)
- name: Register runner
gitlab_runner:
url: https://gitlab.com/api/v4/
private_token: ...5432632464326432632463246...
registration_token: ...4gfdsg345...
name: Docker Machine t1
state: present
active: True
tag_list: ['docker']
run_untagged: False
locked: False
access_level: ref_protected
register: api
# Delete a runner
- name: Delete runner
gitlab_runner:
url: https://gitlab.com/api/v4/
private_token: ...5432632464326432632463246...
registration_token: ...4gfdsg345...
name: Docker Machine t1
state: absent
register: api
'''
RETURN = '''
changed:
description: Values changed on the API
returned: changed
type: boolean
sample: false
msg:
description: Information returned from the API when updating a runner, a create only returns the id and token.
returned: always
type: dict
sample: {
"id": 31,
"description": "Docker Machine t2",
"active": true,
"is_shared": true,
"name": null,
"online": null,
"status": "not_connected",
"tag_list": [
"docker"
],
"run_untagged": true,
"locked": false,
"maximum_timeout": null,
"access_level": "ref_protected",
"version": null,
"revision": null,
"platform":null,
"architecture": null,
"contacted_at": null,
"token": "ba4be.....e6a3b8",
"projects": [],
"groups": []
}
token:
description: Runner token of affected runner
returned: when registered or updated runner
type: str
sample: ["2a5aeecc61dc98c4d780b14b330e3282"]
'''
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
import json
class AnsibleGitlabAPI(AnsibleModule):
def __init__(self, module, url, private_token):
self._module = module
self._auth_header = {'PRIVATE-TOKEN': private_token}
self.url = url
self.timeout = module.params['api_timeout']
def check_response(self, info, response, api_call):
"""
Checks response code.
Returns: response in JSON.
"""
if info['status'] in (200, 201):
return json.loads(response.read())
elif info['status'] == 204:
return json.loads('{"msg":"Request completed"}')
elif info['status'] in (403, 404):
return None
else:
self._module.fail_json(msg='Failure while calling the GitLab API for '
'"%s".' % api_call, fetch_url_info=info)
def _get(self, api_call):
resp, info = fetch_url(self._module, self.url + api_call,
headers=self._auth_header,
timeout=self.timeout)
return self.check_response(info, resp, api_call)
def _post(self, api_call, data=None):
"""
Sends POST request.
Returns: response.
"""
headers = self._auth_header.copy()
if data is not None:
data = self._module.jsonify(data)
headers['Content-type'] = 'application/json'
resp, info = fetch_url(self._module,
self.url + api_call,
headers=headers,
method='POST',
data=data,
timeout=self.timeout)
return self.check_response(info, resp, api_call)
def _put(self, api_call, data=None):
"""
Sends PUT request.
Returns: response.
"""
headers = self._auth_header.copy()
if data is not None:
data = self._module.jsonify(data)
headers['Content-type'] = 'application/json'
resp, info = fetch_url(self._module,
self.url + api_call,
headers=headers,
method='PUT',
data=data,
timeout=self.timeout)
return self.check_response(info, resp, api_call)
def _delete(self, api_call):
"""
Sends DELETE request.
Returns: response.
"""
resp, info = fetch_url(self._module,
self.url + api_call,
headers=self._auth_header,
method='DELETE',
timeout=self.timeout)
return self.check_response(info, resp, api_call)
def get_runner_id(self, description):
"""
Gets the ID for a given description.
Returns: ID as int.
"""
r = self._get('runners/all')
if r is None:
return None
for runner in r:
if runner['description'] == description:
return runner.get('id')
return None
def delete_runner(self, id):
"""
Sends DELETE request for runner with the same ID.
Returns: response.
"""
return self._delete('runners/' + str(id))
def update_runner(self, id, runner):
"""
Sends UPDATE request for runner to change all the fields.
Returns: response.
"""
form_data = runner.get_dict()
return self._put('runners/' + str(id), form_data)
def register_runner(self, runner, registration_token):
"""
Sends POST request to register runner.
Returns: response.
"""
form_data = runner.get_dict()
form_data["token"] = registration_token
return self._post('runners/', form_data)
def get_runner_list_short(self):
"""
Sends GET request to get a global list of all runners.
Returns: JSON list.
"""
return self._get('runners/all')
def get_runner_list(self, ids, primary_key):
"""
Sends GET requests to get details of all runners.
Returns: JSON list.
"""
d = {}
for i in ids:
runner_details = self.get_runner_details(i)
d[runner_details.get(primary_key)] = runner_details
return d
def get_runner_details(self, id):
"""
Sends GET request to get the details of a specific runner .
Returns: JSON list.
"""
return self._get('runners/' + str(id))
class Runner():
def __init__(self, id, description, active, tag_list, run_untagged, locked, access_level, maximum_timeout):
self.id = id
self.description = description
self.active = active
self.tag_list = tag_list
self.run_untagged = run_untagged
self.locked = locked
self.access_level = access_level
self.maximum_timeout = maximum_timeout
def __eq__(self, other):
"""
Compare every field and its value with the fields and values of another instance of this class.
Returns: boolean.
"""
return self.__dict__ == other.__dict__
def get_dict(self):
"""
Returns every field and its value of this class.
Returns: dict.
"""
return self.__dict__
def get_gitlab_argument_spec():
"""
Returns argument spec with all optional or required Ansible arguments.
Returns: dict.
"""
return dict(
private_token=dict(
fallback=(env_fallback, ['GITLAB_PRIVATE_TOKEN']),
no_log=True,
required=True),
name=dict(required=True, type='str'),
active=dict(required=False, type='bool', default=True, choices=[True, False]),
tag_list=dict(required=False, type='list',
default=["docker"]),
run_untagged=dict(
required=False, type='bool', default=True),
locked=dict(required=False, type='bool', default=False, choices=[True, False]),
access_level=dict(required=False, type='str',
default='ref_protected', choices=["ref_protected", "not_protected"]),
maximum_timeout=dict(
required=False, type='int', default=3600),
api_timeout=dict(default=30, type='int'),
url=dict(required=False, type='str',
default="https://gitlab.com/api/v4/"),
registration_token=dict(required=True, type='str'),
state=dict(required=False, type='str', default="present", choices=["present", "absent"]),
)
def main():
argument_spec = get_gitlab_argument_spec()
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
target_state = module.params['state']
description = module.params['name']
private_token = module.params['private_token']
url = module.params['url']
active = module.params['active']
tag_list = module.params['tag_list']
run_untagged = module.params['run_untagged']
locked = module.params['locked']
access_level = module.params['access_level']
maximum_timeout = module.params['maximum_timeout']
api = AnsibleGitlabAPI(module, url, private_token)
id = api.get_runner_id(description)
target_runner = Runner(id, description, active, tag_list,
run_untagged, locked, access_level, maximum_timeout)
token = None
api_runner = None
response = None
changed = False
# Check if runner needs to be registered, updated or deleted
# Don't actually change anything if module is in check_mode (dry run)
if target_state == 'present':
if id is None:
if not module.check_mode:
response = api.register_runner(
target_runner, module.params["registration_token"])
token = response['token']
changed = True
else:
api_runner_details = api.get_runner_details(id)
response = api_runner_details
token = api_runner_details['token']
id = api_runner_details['id']
description = api_runner_details['description']
active = api_runner_details['active']
tag_list = api_runner_details['tag_list']
locked = api_runner_details['locked']
access_level = api_runner_details['access_level']
maximum_timeout = api_runner_details['maximum_timeout']
api_runner = Runner(id, description, active, tag_list,
run_untagged, locked, access_level, maximum_timeout)
if api_runner == target_runner:
changed = False
else:
if not module.check_mode:
response = api.update_runner(id, target_runner)
changed = True
elif target_state == 'absent':
if id is None:
changed = False
else:
if not module.check_mode:
response = api.delete_runner(id)
changed = True
module.exit_json(changed=changed, msg=response, token=token)
if __name__ == '__main__':
main()