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:
Thierry Carrez 2020-01-10 15:42:56 +01:00
parent bfe28f2601
commit 7be4b337bb
5 changed files with 189 additions and 0 deletions

View 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 }}"

View 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.

View 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()

View 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 }}

View File

@ -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: |