Add gate-quickstart role
This role lets arbitrary tripleo-quickstart and tripleo-quickstart-extras changes be gated together through the "Depends-On" mechanism. Change-Id: If5b747b1bba17dd20efa9e7caee6fbc638dbb716
This commit is contained in:
parent
661358a588
commit
ffb30adaea
8
playbooks/gate-quickstart.yml
Normal file
8
playbooks/gate-quickstart.yml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
# This playbooks is responsible for gating changes in tripleo-quickstart or
|
||||
# tripleo-quickstart-extras with a possibility to depend on changes from the
|
||||
# other repo
|
||||
- name: Gate quickstart changes
|
||||
hosts: localhost
|
||||
roles:
|
||||
- gate-quickstart
|
3
roles/gate-quickstart/defaults/main.yml
Normal file
3
roles/gate-quickstart/defaults/main.yml
Normal file
@ -0,0 +1,3 @@
|
||||
gated_projects:
|
||||
- openstack/tripleo-quickstart
|
||||
- openstack/tripleo-quickstart-extras
|
212
roles/gate-quickstart/library/jenkins_deps.py
Normal file
212
roles/gate-quickstart/library/jenkins_deps.py
Normal file
@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: jenkins_deps
|
||||
version_added: "2.0"
|
||||
short_description: Parses the Gerrit commit message and identifies cross repo depenency changes
|
||||
description:
|
||||
- Parses the Gerrit commit message and identifies cross repo depenency changes.
|
||||
The expected format in the commit message is:
|
||||
Depends-On: <change-id>[@<gerrit-instance-shorthand>]
|
||||
Where <change-id> is the gerrit Change-Id of the dependent change,
|
||||
<gerrit-instance> should be a part of a hostname in ALLOWED_HOSTS.
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
- The hostname of the Gerrit server.
|
||||
required: True
|
||||
change_id:
|
||||
description:
|
||||
- The change-id of the Gerrit change, starting with I...
|
||||
required: True
|
||||
branch:
|
||||
description:
|
||||
- The branch of the change.
|
||||
required: True
|
||||
patchset_rev:
|
||||
description:
|
||||
- The sha hash of the patchset to be tested. Latest will be used if omitted.
|
||||
required: False
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- jenkins-deps:
|
||||
host: review.openstack.org
|
||||
change_id: I387b6bfd763d2d86cad68a3119b0edd0caa237b0
|
||||
patchset_rev: d18f21853e2f3be7382a20d0f42232ff3a78b348
|
||||
'''
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
# we ignore any other host reference
|
||||
ALLOWED_HOSTS = ['review.openstack.org',
|
||||
'review.gerrithub.io',
|
||||
'review.rdoproject.org']
|
||||
|
||||
|
||||
def parse_commit_msg(current_host, msg):
|
||||
'''Look for dependency links in the commit message.'''
|
||||
tags = []
|
||||
for line in iter(msg.splitlines()):
|
||||
# note: this regexp takes care of sanitizing the input
|
||||
tag = re.search(r'Depends-On: *(I[0-9a-f]+)@?([0-9a-z\.\-:]*)',
|
||||
line, re.IGNORECASE)
|
||||
if tag:
|
||||
change_id = tag.group(1)
|
||||
target = tag.group(2)
|
||||
if target == '':
|
||||
host = current_host
|
||||
else:
|
||||
# match a shorthand hostname for our allowed list
|
||||
for hostname in ALLOWED_HOSTS:
|
||||
if target in hostname:
|
||||
host = hostname
|
||||
break
|
||||
else:
|
||||
logging.warning('Cannot resolve "%s" to a host from the '
|
||||
'ALLOWED HOSTS list', target)
|
||||
continue
|
||||
tags.append({'host': host,
|
||||
'change_id': change_id,
|
||||
'branch': None,
|
||||
'revision': None})
|
||||
return tags
|
||||
|
||||
|
||||
def get_details(host, change_id, branch, revision):
|
||||
'''Get the details of a specific change'''
|
||||
url = ''.join(['https://', host, '/changes/?q=change:', change_id])
|
||||
try:
|
||||
req = requests.get(url)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
return {'fail_msg': ''.join(['warning: failed to query change '
|
||||
'details from ', url])}
|
||||
# strip XSSI attack prevention prefix
|
||||
data = json.loads(req.text[4:])
|
||||
if len(data) == 0:
|
||||
return {'fail_msg': ''.join(['warning: no change found with id ',
|
||||
change_id, ' at ', url])}
|
||||
elif len(data) == 1:
|
||||
# not filtering by branch if not necessary
|
||||
full_id = data[0]['id']
|
||||
else:
|
||||
# there are more than one change with the same ID
|
||||
try:
|
||||
full_id = [change['id'] for change
|
||||
in data if change['branch'] == branch][0]
|
||||
except IndexError:
|
||||
return {'fail_msg': ''.join(['warning: no change found with id ',
|
||||
change_id, ' for branch ', branch,
|
||||
' at ', url])}
|
||||
url = ''.join(['https://', host, '/changes/', full_id,
|
||||
'?o=ALL_REVISIONS&o=ALL_COMMITS'])
|
||||
try:
|
||||
req = requests.get(url)
|
||||
req.raise_for_status()
|
||||
except requests.exceptions.HTTPError:
|
||||
return {'fail_msg': ''.join(['warning: failed to fetch details of ',
|
||||
change_id, ' from ', url])}
|
||||
# strip XSSI attack prevention prefix
|
||||
data = json.loads(req.text[4:])
|
||||
if revision is None:
|
||||
revision = data['current_revision']
|
||||
if revision not in data['revisions']:
|
||||
return {'fail_msg': ''.join(['warning: cannot find revision ',
|
||||
revision, ' of change ', change_id,
|
||||
' at ', url])}
|
||||
return {'host': host,
|
||||
'change_id': str(data['change_id']),
|
||||
'project': str(data['project']),
|
||||
'branch': str(data['branch']),
|
||||
'refspec': str(data['revisions'][revision]['ref']),
|
||||
'commit_msg':
|
||||
str(data['revisions'][revision]['commit']['message'])}
|
||||
|
||||
|
||||
def resolve_dep(host, change_id, branch, revision):
|
||||
'''
|
||||
Resolve the dependencies in the target commits until there are no more
|
||||
dependent changes. If the branch or revision is None, it can still resolve
|
||||
the dependencies. It only uses the branch when the change_id is ambigiuous
|
||||
and by default uses the latest patchset's revision.
|
||||
|
||||
The function avoids circular dependencies and only allows one change per
|
||||
project to be added to the output list.
|
||||
|
||||
Returns a list of dictionaries with the dependent changes.
|
||||
'''
|
||||
resolved_ids = []
|
||||
deps = []
|
||||
to_resolve = [{'host': host,
|
||||
'change_id': change_id,
|
||||
'branch': branch,
|
||||
'revision': revision}]
|
||||
output_msg = []
|
||||
while len(to_resolve) > 0:
|
||||
change = to_resolve.pop()
|
||||
# use the original branch as default
|
||||
if change['branch'] is None:
|
||||
change['branch'] = branch
|
||||
|
||||
# avoid circular dependencies
|
||||
if change['change_id'] in resolved_ids:
|
||||
continue
|
||||
|
||||
details = get_details(**change)
|
||||
if 'fail_msg' in details:
|
||||
output_msg.append(details['fail_msg'])
|
||||
continue
|
||||
resolved_ids.append(details['change_id'])
|
||||
|
||||
# allow only one of each project as a dependency
|
||||
if details['project'] not in (d['project'] for d in deps):
|
||||
deps.append({'host': change['host'],
|
||||
'project': details['project'],
|
||||
'branch': details['branch'],
|
||||
'refspec': details['refspec']})
|
||||
else:
|
||||
output_msg.append(
|
||||
''.join(['warning: skipping ', change['change_id'], ' on ',
|
||||
change['host'], ' because project "',
|
||||
details['project'], '" is already a dependency']))
|
||||
continue
|
||||
new_deps = parse_commit_msg(change['host'], details['commit_msg'])
|
||||
to_resolve.extend(new_deps)
|
||||
if len(deps) == 0:
|
||||
output_msg.append('error: failed to resolve the target change')
|
||||
return {'failed': True,
|
||||
'msg': ', '.join(output_msg)}
|
||||
else:
|
||||
return {'changed': True,
|
||||
'ansible_facts': {'artg_change_list': deps},
|
||||
'msg': ', '.join(output_msg)}
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
host=dict(required=True, type='str'),
|
||||
change_id=dict(required=True, type='str'),
|
||||
branch=dict(required=False, default=None, type='str'),
|
||||
patchset_rev=dict(required=False, default=None, type='str')
|
||||
)
|
||||
)
|
||||
result = resolve_dep(module.params['host'],
|
||||
module.params['change_id'],
|
||||
module.params['branch'],
|
||||
module.params['patchset_rev'])
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
# see http://docs.ansible.com/developing_modules.html#common-module-boilerplate
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
23
roles/gate-quickstart/tasks/checkout.yml
Normal file
23
roles/gate-quickstart/tasks/checkout.yml
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
- name: Get the project name
|
||||
set_fact:
|
||||
gated_project_name: '{{ gated_change.project | regex_replace("^.*/", "") }}'
|
||||
|
||||
- name: Check out the gated change
|
||||
git:
|
||||
repo: 'https://{{ gated_change.host }}/{{ gated_change.project }}'
|
||||
dest: '{{ lookup("env", "WORKSPACE") }}/{{ gated_project_name }}'
|
||||
refspec: '{{ gated_change.refspec }}'
|
||||
version: 'FETCH_HEAD'
|
||||
|
||||
- when: gated_project_name != "tripleo-quickstart"
|
||||
block:
|
||||
- name: Modify requirements
|
||||
replace:
|
||||
dest: '{{ lookup("env", "WORKSPACE") }}/tripleo-quickstart/quickstart-extras-requirements.txt'
|
||||
regexp: '^.*egg={{ gated_project_name }}$'
|
||||
replace: 'file://{{ lookup("env", "WORKSPACE") }}/{{ gated_project_name }}/#egg={{ gated_project_name }}'
|
||||
- name: Make sure the package is not installed in our venv
|
||||
pip:
|
||||
name: '{{ gated_project_name }}'
|
||||
state: absent
|
15
roles/gate-quickstart/tasks/main.yml
Normal file
15
roles/gate-quickstart/tasks/main.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
# This playbooks is responsible for gating quickstart and the extra roles
|
||||
# together with optional dependencies between them.
|
||||
- name: Parse Jenkins changes
|
||||
jenkins_deps:
|
||||
host: "{{ lookup('env', 'GERRIT_HOST') }}"
|
||||
change_id: "{{ lookup('env', 'GERRIT_CHANGE_ID') }}"
|
||||
branch: "{{ lookup('env', 'GERRIT_BRANCH') }}"
|
||||
patchset_rev: "{{ lookup('env', 'GERRIT_PATCHSET_REVISION') }}"
|
||||
when: artg_change_list is not defined and "{{ lookup('env', 'GERRIT_HOST') }}" != ""
|
||||
|
||||
- name: Check out the specific changes
|
||||
include: checkout.yml gated_change={{ item }}
|
||||
with_items: '{{ artg_change_list }}'
|
||||
when: item.project in gated_projects and item.project != "{{ lookup('env', 'GERRIT_PROJECT') }}"
|
Loading…
Reference in New Issue
Block a user