Add freeze-job subcommand

This adds support for the freeze-job endpoint and displays
information about a job as it would run in a pipeline.

Because there is so much data, the text formatter only
displays a high-level summary of the most interesting parts
of a job.  The JSON formatter returns everything.

Change-Id: Ia3e00bf10eae0d569aa49773e81bc8bab1584ba7
This commit is contained in:
James E. Blair 2022-07-16 13:45:56 -07:00
parent f999949aed
commit f96ddd00fc
7 changed files with 168 additions and 0 deletions

View File

@ -164,6 +164,27 @@ Note that zero values for ``oldrev`` and ``newrev`` can indicate
branch creation and deletion; the source code of Zuul is the best reference branch creation and deletion; the source code of Zuul is the best reference
for these more advanced operations. for these more advanced operations.
Freeze-job
^^^^^^^^^^
Display information about a job as it would be run in a particular
project's pipeline. This causes Zuul to combine all of the matching
jobs and variants that would be used to form the final version of a
job that would be executed for a change or ref as enqueued into the
specified pipeline. This includes job attributes, playbook paths,
nodesets, variables, etc. Secret names may be included but the values
are redacted.
The default text output shows an abbreviated summary of only the most
pertinent information. The JSON output reports all available
information.
.. program-output:: zuul-client freeze-job --help
Example::
zuul-client freeze-job --tenant mytenant --pipeline check --project org/project --branch master --job tox
Job-graph Job-graph
^^^^^^^^^ ^^^^^^^^^

View File

@ -0,0 +1,5 @@
---
features:
- |
Add freeze-job subcommand to display information about jobs as they would
be run in a project's pipeline.

View File

@ -488,3 +488,25 @@ GuS6/ewjS+arA1Iyeg/IxmECAwEAAQ==
'https://fake.zuul/api/tenant/tenant1/pipeline/check/' 'https://fake.zuul/api/tenant/tenant1/pipeline/check/'
'project/project1/branch/master/freeze-jobs') 'project/project1/branch/master/freeze-jobs')
self.assertEqual(fakejson, graph) self.assertEqual(fakejson, graph)
def test_freeze_job(self):
"""Test freeze-job endpoint"""
client = ZuulRESTClient(url='https://fake.zuul/')
# test status checks
self._test_status_check(
client, 'get', client.freeze_jobs,
'tenant1', 'check', 'project1', 'master')
fakejson = {
"job": "testjob",
"ansible_version": "5",
}
req = FakeRequestResponse(200, fakejson)
client.session.get = MagicMock(return_value=req)
client.info_ = {}
job = client.freeze_job('tenant1', 'check', 'project1', 'master',
'testjob')
client.session.get.assert_any_call(
'https://fake.zuul/api/tenant/tenant1/pipeline/check/'
'project/project1/branch/master/freeze-job/testjob')
self.assertEqual(fakejson, job)

View File

@ -649,3 +649,45 @@ verify_ssl=True"""
'project/project1/branch/master/freeze-jobs', 'project/project1/branch/master/freeze-jobs',
) )
self.assertEqual(0, exit_code) self.assertEqual(0, exit_code)
def test_freeze_job(self):
"""Test freeze-job subcommand"""
ZC = ZuulClient()
with patch('requests.Session') as mock_sesh:
session = mock_sesh.return_value
fakejson = {
"job": "testjob",
"ansible_version": "5",
"nodeset": {
"groups": [],
"name": "ubuntu-jammy",
},
"vars": {},
"pre_playbooks": [
{
"branch": "master",
"connection": "gerrit",
"path": "playbooks/base/pre.yaml",
"project": "opendev/base-jobs",
"roles": [],
"trusted": True,
},
]
}
session.get = MagicMock(
side_effect=mock_get(
MagicMock(return_value=FakeRequestResponse(200, fakejson))
)
)
exit_code = ZC._main(
['--zuul-url', 'https://fake.zuul', 'freeze-job',
'--tenant', 'tenant1',
'--pipeline', 'check',
'--project', 'project1',
'--branch', 'master',
'--job', 'testjob'])
session.get.assert_any_call(
'https://fake.zuul/api/tenant/tenant1/pipeline/check/'
'project/project1/branch/master/freeze-job/testjob',
)
self.assertEqual(0, exit_code)

View File

@ -305,3 +305,15 @@ class ZuulRESTClient(object):
req = self.session.get(url) req = self.session.get(url)
self._check_request_status(req) self._check_request_status(req)
return req.json() return req.json()
def freeze_job(self, tenant, pipeline, project, branch, job):
suffix = (f'pipeline/{pipeline}/project/{project}/'
f'branch/{branch}/freeze-job/{job}')
if self.info.get("tenant"):
self._check_scope(tenant)
else:
suffix = f'tenant/{tenant}/{suffix}'
url = urllib.parse.urljoin(self.base_url, suffix)
req = self.session.get(url)
self._check_request_status(req)
return req.json()

View File

@ -109,6 +109,7 @@ class ZuulClient():
self.add_builds_list_subparser(subparsers) self.add_builds_list_subparser(subparsers)
self.add_build_info_subparser(subparsers) self.add_build_info_subparser(subparsers)
self.add_job_graph_subparser(subparsers) self.add_job_graph_subparser(subparsers)
self.add_freeze_job_subparser(subparsers)
return subparsers return subparsers
@ -814,6 +815,32 @@ class ZuulClient():
print(formatted_result) print(formatted_result)
return True return True
def add_freeze_job_subparser(self, subparsers):
cmd_freeze_job = subparsers.add_parser(
'freeze-job', help='Freeze and display a job')
cmd_freeze_job.add_argument(
'--tenant', help='tenant name', required=False, default='')
cmd_freeze_job.add_argument('--pipeline', help='pipeline name',
required=True)
cmd_freeze_job.add_argument('--project', help='project name',
required=True)
cmd_freeze_job.add_argument('--branch', help='branch name',
required=True)
cmd_freeze_job.add_argument('--job', help='job name',
required=True)
cmd_freeze_job.set_defaults(func=self.freeze_job)
self.cmd_freeze_job = cmd_freeze_job
def freeze_job(self):
client = self.get_client()
self._check_tenant_scope(client)
job = client.freeze_job(self.tenant(), self.args.pipeline,
self.args.project, self.args.branch,
self.args.job)
formatted_result = self.formatter('FreezeJob')(job)
print(formatted_result)
return True
def main(): def main():
ZuulClient().main() ZuulClient().main()

View File

@ -15,6 +15,7 @@
import time import time
from dateutil.parser import isoparse from dateutil.parser import isoparse
import pprint
import prettytable import prettytable
import json import json
@ -66,6 +67,9 @@ class BaseFormatter:
def formatJobGraph(self, data): def formatJobGraph(self, data):
raise NotImplementedError raise NotImplementedError
def formatFreezeJob(self, data):
raise NotImplementedError
class JSONFormatter(BaseFormatter): class JSONFormatter(BaseFormatter):
def __call__(self, data) -> str: def __call__(self, data) -> str:
@ -285,6 +289,41 @@ class PrettyTableFormatter(BaseFormatter):
]) ])
return str(table) return str(table)
def formatFreezeJob(self, data) -> str:
printer = pprint.PrettyPrinter(indent=2)
ret = ''
for label, key in [
('Job', 'job'),
('Branch', 'branch'),
('Ansible Version', 'ansible_version'),
('Timeout', 'timeout'),
('Post Timeout', 'post_timeout'),
('Workspace Scheme', 'workspace_scheme'),
('Override Checkout', 'override_checkout'),
]:
value = data.get(key)
if value is not None:
ret += f'{label}: {value}\n'
if data['nodeset']['name']:
ret += f"Nodeset: {data['nodeset']['name']}\n"
for label, key in [
('Pre-run Playbooks', 'pre_playbooks'),
('Run Playbooks', 'playbooks'),
('Post-run Playbooks', 'post_playbooks'),
('Cleanup Playbooks', 'cleanup_playbooks'),
]:
pbs = data.get(key)
if not pbs:
continue
ret += f"{label}:\n"
for pb in pbs:
trusted = ' [trusted]' if pb['trusted'] else ''
ret += (f" {pb['connection']}:{pb['project']}:"
f"{pb['path']}@{pb['branch']}{trusted}\n")
ret += 'Vars:\n'
ret += printer.pformat(data['vars'])
return ret
class DotFormatter(BaseFormatter): class DotFormatter(BaseFormatter):
"""Format for graphviz""" """Format for graphviz"""