From 7be4b337bb10ea33e15c9e4a39fc5b7af12c06e0 Mon Sep 17 00:00:00 2001 From: Thierry Carrez Date: Fri, 10 Jan 2020 15:42:56 +0100 Subject: [PATCH] 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 --- playbooks/check-release-approval/run.yaml | 6 + roles/check-release-approval/README.rst | 17 ++ .../files/check_approval.py | 149 ++++++++++++++++++ roles/check-release-approval/tasks/main.yaml | 3 + zuul.d/jobs.yaml | 14 ++ 5 files changed, 189 insertions(+) create mode 100644 playbooks/check-release-approval/run.yaml create mode 100644 roles/check-release-approval/README.rst create mode 100755 roles/check-release-approval/files/check_approval.py create mode 100644 roles/check-release-approval/tasks/main.yaml diff --git a/playbooks/check-release-approval/run.yaml b/playbooks/check-release-approval/run.yaml new file mode 100644 index 0000000000..72b606e4f4 --- /dev/null +++ b/playbooks/check-release-approval/run.yaml @@ -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 }}" diff --git a/roles/check-release-approval/README.rst b/roles/check-release-approval/README.rst new file mode 100644 index 0000000000..e3f2a9f7a9 --- /dev/null +++ b/roles/check-release-approval/README.rst @@ -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. diff --git a/roles/check-release-approval/files/check_approval.py b/roles/check-release-approval/files/check_approval.py new file mode 100755 index 0000000000..4337917d12 --- /dev/null +++ b/roles/check-release-approval/files/check_approval.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# +# Check PTL/liaison has approved release +# +# Copyright 2019 Thierry Carrez +# 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() diff --git a/roles/check-release-approval/tasks/main.yaml b/roles/check-release-approval/tasks/main.yaml new file mode 100644 index 0000000000..b0b13536c9 --- /dev/null +++ b/roles/check-release-approval/tasks/main.yaml @@ -0,0 +1,3 @@ +--- +- name: Check for PTL or release liaison approval on the current Gerrit change + script: check_approval.py {{ change }} {{ releases }} {{ governance }} diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml index b09b858cda..028704b5c3 100644 --- a/zuul.d/jobs.yaml +++ b/zuul.d/jobs.yaml @@ -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: |