Define check-release-approval executor job
As discussed in [1], create a job that will check that a release request has been approved by the PTL or a release liaison attached to the deliverable. Since this job is very short and will be rerun every time a vote is posted, rather than consume a remote host, it will be run directly on the executor. To that effect, the python script called only uses stdlib or modules that are already present on the executor (requests and yaml). [1] http://lists.openstack.org/pipermail/openstack-infra/2019-December/006556.html Change-Id: Ibe1f0fba6ae2a459be22b33d8de4285b739a1df0
This commit is contained in:
parent
bfe28f2601
commit
7be4b337bb
6
playbooks/check-release-approval/run.yaml
Normal file
6
playbooks/check-release-approval/run.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
- hosts: localhost
|
||||
roles:
|
||||
- role: check-release-approval
|
||||
change: "{{ zuul.change }}"
|
||||
releases: "{{ zuul.executor.work_root }}/{{ zuul.project.src_dir }}"
|
||||
governance: "{{ zuul.executor.work_root }}/{{ zuul.projects['opendev.org/openstack/governance'].src_dir }}"
|
17
roles/check-release-approval/README.rst
Normal file
17
roles/check-release-approval/README.rst
Normal file
@ -0,0 +1,17 @@
|
||||
Query Gerrit on release requests, checking for approvals from the PTL or
|
||||
release liaison corresponding to the affected deliverable. Succeed if such
|
||||
approval is present, fail otherwise.
|
||||
|
||||
**Role Variables**
|
||||
|
||||
.. zuul:rolevar:: change
|
||||
|
||||
Gerrit change number. Should be something like: 696104
|
||||
|
||||
.. zuul:rolevar:: releases
|
||||
|
||||
Directory containing the releases repository data.
|
||||
|
||||
.. zuul:rolevar:: governance
|
||||
|
||||
Directory containing the governance repository data.
|
149
roles/check-release-approval/files/check_approval.py
Executable file
149
roles/check-release-approval/files/check_approval.py
Executable file
@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Check PTL/liaison has approved release
|
||||
#
|
||||
# Copyright 2019 Thierry Carrez <thierry@openstack.org>
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import requests
|
||||
from requests.packages import urllib3
|
||||
import yaml
|
||||
|
||||
|
||||
PROJECTS_YAML = 'reference/projects.yaml'
|
||||
GERRIT_URL = 'https://review.opendev.org/'
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# Turn of warnings about bad SSL config.
|
||||
# https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
def get_team(workspace, deliverablefile):
|
||||
with open(os.path.join(workspace, deliverablefile), 'r') as dfile:
|
||||
team = yaml.safe_load(dfile)['team']
|
||||
return team
|
||||
|
||||
|
||||
def get_liaisons(workspace, team):
|
||||
filename = os.path.join(workspace, 'data/release_liaisons.yaml')
|
||||
with open(filename, 'r') as lfile:
|
||||
liaisons = yaml.safe_load(lfile)
|
||||
if team in liaisons:
|
||||
return [i['email'] for i in liaisons[team]]
|
||||
else:
|
||||
print('WARNING: %s team does not exist in liaisons file' % team)
|
||||
return []
|
||||
|
||||
|
||||
class GerritChange(object):
|
||||
|
||||
def __init__(self, args):
|
||||
# Load governance data
|
||||
with open(os.path.join(args.governance, PROJECTS_YAML), 'r') as dfile:
|
||||
self.gov_data = yaml.safe_load(dfile)
|
||||
|
||||
# Grab changeid details from Gerrit
|
||||
call = 'changes/%s' % args.changeid + \
|
||||
'?o=CURRENT_REVISION&o=CURRENT_FILES&o=DETAILED_LABELS' + \
|
||||
'&o=DETAILED_ACCOUNTS'
|
||||
raw = requests.get(GERRIT_URL + call)
|
||||
|
||||
# Gerrit's REST API prepends a JSON-breaker to avoid XSS
|
||||
if raw.text.startswith(")]}'"):
|
||||
trimmed = raw.text[4:]
|
||||
else:
|
||||
trimmed = raw.text
|
||||
|
||||
# Try to decode and bail with much detail if it fails
|
||||
try:
|
||||
decoded = json.loads(trimmed)
|
||||
except Exception:
|
||||
LOG.error(
|
||||
'\nrequest returned %s error to query:\n\n %s\n'
|
||||
'\nwith detail:\n\n %s\n',
|
||||
raw, raw.url, trimmed)
|
||||
raise
|
||||
|
||||
# Instantiate object with retrieved data
|
||||
self.raw_data = decoded
|
||||
self.approvers = [i['email']
|
||||
for i in decoded['labels']['Code-Review']['all']
|
||||
if i['value'] > 0]
|
||||
self.approvers.append(decoded['owner']['email'])
|
||||
currev = decoded['current_revision']
|
||||
self.deliv_files = list(decoded['revisions'][currev]['files'].keys())
|
||||
self.workspace = args.releases
|
||||
|
||||
def is_approved(self):
|
||||
LOG.debug('Approvals: %s' % self.approvers)
|
||||
approved = True
|
||||
for deliv_file in self.deliv_files:
|
||||
team = get_team(self.workspace, deliv_file)
|
||||
try:
|
||||
govteam = self.gov_data[team]
|
||||
except ValueError:
|
||||
print('✕ %s mentions unknown team %s' % (deliv_file, team))
|
||||
approved = False
|
||||
break
|
||||
|
||||
# Check that deliverable is indeed defined in governance team
|
||||
delivname, _ = os.path.splitext(os.path.basename(deliv_file))
|
||||
if delivname not in govteam['deliverables']:
|
||||
print('✕ %s not in %s governance' % (deliv_file, team))
|
||||
approved = False
|
||||
break
|
||||
|
||||
# Fetch PTL and release liaisons
|
||||
liaisons = get_liaisons(self.workspace, team)
|
||||
if 'email' in govteam['ptl']:
|
||||
liaisons.append(govteam['ptl']['email'])
|
||||
LOG.debug('%s needs %s' % (deliv_file, liaisons))
|
||||
|
||||
for approver in self.approvers:
|
||||
if approver in liaisons:
|
||||
print('✓ %s validated by %s' % (deliv_file, approver))
|
||||
break
|
||||
else:
|
||||
print('✕ %s missing PTL/liaison approval' % deliv_file)
|
||||
approved = False
|
||||
return approved
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('changeid')
|
||||
parser.add_argument('releases')
|
||||
parser.add_argument('governance')
|
||||
parser.add_argument("--debug", action='store_true')
|
||||
args = parser.parse_args(args)
|
||||
|
||||
if (args.debug):
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
change = GerritChange(args)
|
||||
|
||||
if not change.is_approved():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
3
roles/check-release-approval/tasks/main.yaml
Normal file
3
roles/check-release-approval/tasks/main.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
- name: Check for PTL or release liaison approval on the current Gerrit change
|
||||
script: check_approval.py {{ change }} {{ releases }} {{ governance }}
|
@ -1133,6 +1133,20 @@
|
||||
- name: afs
|
||||
secret: afsadmin_keytab
|
||||
|
||||
- job:
|
||||
name: check-release-approval
|
||||
description: |
|
||||
Checks that release was approved by PTL or release liaison.
|
||||
files:
|
||||
- ^deliverables/.*$
|
||||
required-projects:
|
||||
- name: openstack/governance
|
||||
run: playbooks/check-release-approval/run.yaml
|
||||
final: true
|
||||
timeout: 120
|
||||
nodeset:
|
||||
nodes: []
|
||||
|
||||
- job:
|
||||
name: tag-releases
|
||||
description: |
|
||||
|
Loading…
Reference in New Issue
Block a user