Get executor job params

Move the param generation for an execution job into a library for reuse.
The param preparation takes care of determining projects and connections
for dependant roles which saves a zuul-runner from needing to understand
canonical names for which it would need to query a scheduler.

Implement a basic API to freeze and grab these job params. These could
then be passed to another zuul-executor or other runner.

Change-Id: I681f2a3384c9a65ae0acc3fce966e8ec47005b64
Co-Authored-By: Tristan Cacqueray <tdecacqu@redhat.com>
This commit is contained in:
Joshua Hesketh 2018-09-29 14:04:48 +10:00 committed by Tristan Cacqueray
parent 13923aa737
commit 7d424ee477
6 changed files with 377 additions and 165 deletions

View File

@ -989,6 +989,96 @@ class TestWeb(BaseTestWeb):
}
self.assertIn(expected, resp.json())
def test_freeze_job(self):
resp = self.get_url(
"api/tenant/tenant-one/pipeline/check"
"/project/org/project1/branch/master/freeze-job/"
"project-test1")
job_params = {
'job': 'project-test1',
'ansible_version': '2.9',
'timeout': None,
'post_timeout': None,
'items': [],
'projects': [],
'branch': 'master',
'cleanup_playbooks': [],
'groups': [],
'nodes': [{
'comment': None,
'hold_job': None,
'label': 'label1',
'name': ['controller'],
'state': 'unknown'
}],
'override_branch': None,
'override_checkout': None,
'repo_state': {},
'playbooks': [{
'connection': 'gerrit',
'project': 'common-config',
'branch': 'master',
'trusted': True,
'roles': [{
'target_name': 'common-config',
'type': 'zuul',
'project_canonical_name':
'review.example.com/common-config',
'implicit': True,
'project_default_branch': 'master',
'connection': 'gerrit',
'project': 'common-config',
}],
'secrets': {},
'path': 'playbooks/project-test1.yaml',
}],
'pre_playbooks': [],
'post_playbooks': [],
'ssh_keys': [],
'vars': {},
'extra_vars': {},
'host_vars': {},
'group_vars': {},
'zuul': {
'_inheritance_path': [
'<Job base branches: None source: '
'common-config/zuul.yaml@master#53>',
'<Job project-test1 branches: None source: '
'common-config/zuul.yaml@master#66>',
'<Job project-test1 branches: None source: '
'common-config/zuul.yaml@master#138>',
'<Job project-test1 branches: None source: '
'common-config/zuul.yaml@master#53>'],
'build': '00000000000000000000000000000000',
'buildset': None,
'branch': 'master',
'ref': None,
'pipeline': 'check',
'post_review': False,
'job': 'project-test1',
'voting': True,
'project': {
'name': 'org/project1',
'short_name': 'project1',
'canonical_hostname': 'review.example.com',
'canonical_name': 'review.example.com/org/project1',
'src_dir': 'src/review.example.com/org/project1',
},
'tenant': 'tenant-one',
'timeout': None,
'jobtags': [],
'branch': 'master',
'projects': {},
'items': [],
'child_jobs': [],
'event_id': None,
},
}
self.assertEqual(job_params, resp.json())
class TestWebMultiTenant(BaseTestWeb):
tenant_config_file = 'config/multi-tenant/main.yaml'
@ -1015,6 +1105,16 @@ class TestWebSecrets(BaseTestWeb):
secret = {'name': 'project1_secret', 'alias': 'secret_name'}
self.assertEqual([secret], run[0]['secrets'])
def test_freeze_job_redacted(self):
# Test that ssh_keys and secrets are redacted
resp = self.get_url(
"api/tenant/tenant-one/pipeline/check"
"/project/org/project1/branch/master/freeze-job/"
"project1-secret").json()
self.assertEqual(
{'secret_name': 'REDACTED'}, resp['playbooks'][0]['secrets'])
self.assertEqual('REDACTED', resp['ssh_keys'][0]['key'])
class TestInfo(ZuulDBTestCase, BaseTestWeb):

View File

@ -15,12 +15,12 @@
import gear
import json
import logging
import os
import time
import threading
from uuid import uuid4
import zuul.model
import zuul.executor.common
from zuul.lib.config import get_default
from zuul.lib.gear_utils import getGearmanFunctions
from zuul.lib.jsonutil import json_dumps
@ -140,7 +140,6 @@ class ExecutorClient(object):
def execute(self, job, item, pipeline, dependent_changes=[],
merger_items=[]):
log = get_annotated_logger(self.log, item.event)
tenant = pipeline.tenant
uuid = str(uuid4().hex)
nodeset = item.current_build_set.getJobNodeSet(job.name)
log.info(
@ -148,165 +147,13 @@ class ExecutorClient(object):
"with dependent changes %s",
job, uuid, nodeset, item.change, dependent_changes)
project = dict(
name=item.change.project.name,
short_name=item.change.project.name.split('/')[-1],
canonical_hostname=item.change.project.canonical_hostname,
canonical_name=item.change.project.canonical_name,
src_dir=os.path.join('src', item.change.project.canonical_name),
)
params = zuul.executor.common.construct_gearman_params(
uuid, self.sched, nodeset,
job, item, pipeline, dependent_changes, merger_items,
redact_secrets_and_keys=False)
# TODO: deprecate and remove this variable?
params["zuul"]["_inheritance_path"] = list(job.inheritance_path)
zuul_params = dict(build=uuid,
buildset=item.current_build_set.uuid,
ref=item.change.ref,
pipeline=pipeline.name,
post_review=pipeline.post_review,
job=job.name,
voting=job.voting,
project=project,
tenant=tenant.name,
timeout=job.timeout,
event_id=item.event.zuul_event_id,
jobtags=sorted(job.tags),
_inheritance_path=list(job.inheritance_path))
if job.artifact_data:
zuul_params['artifacts'] = job.artifact_data
if job.override_checkout:
zuul_params['override_checkout'] = job.override_checkout
if hasattr(item.change, 'branch'):
zuul_params['branch'] = item.change.branch
if hasattr(item.change, 'tag'):
zuul_params['tag'] = item.change.tag
if hasattr(item.change, 'number'):
zuul_params['change'] = str(item.change.number)
if hasattr(item.change, 'url'):
zuul_params['change_url'] = item.change.url
if hasattr(item.change, 'patchset'):
zuul_params['patchset'] = str(item.change.patchset)
if hasattr(item.change, 'message'):
zuul_params['message'] = item.change.message
if (hasattr(item.change, 'oldrev') and item.change.oldrev
and item.change.oldrev != '0' * 40):
zuul_params['oldrev'] = item.change.oldrev
if (hasattr(item.change, 'newrev') and item.change.newrev
and item.change.newrev != '0' * 40):
zuul_params['newrev'] = item.change.newrev
zuul_params['projects'] = {} # Set below
zuul_params['items'] = dependent_changes
zuul_params['child_jobs'] = list(item.job_graph.getDirectDependentJobs(
job.name))
params = dict()
params['job'] = job.name
params['timeout'] = job.timeout
params['post_timeout'] = job.post_timeout
params['items'] = merger_items
params['projects'] = []
if hasattr(item.change, 'branch'):
params['branch'] = item.change.branch
else:
params['branch'] = None
params['override_branch'] = job.override_branch
params['override_checkout'] = job.override_checkout
params['repo_state'] = item.current_build_set.repo_state
params['ansible_version'] = job.ansible_version
def make_playbook(playbook):
d = playbook.toDict()
for role in d['roles']:
if role['type'] != 'zuul':
continue
project_metadata = item.layout.getProjectMetadata(
role['project_canonical_name'])
if project_metadata:
role['project_default_branch'] = \
project_metadata.default_branch
else:
role['project_default_branch'] = 'master'
role_trusted, role_project = item.layout.tenant.getProject(
role['project_canonical_name'])
role_connection = role_project.source.connection
role['connection'] = role_connection.connection_name
role['project'] = role_project.name
return d
if job.name != 'noop':
params['playbooks'] = [make_playbook(x) for x in job.run]
params['pre_playbooks'] = [make_playbook(x) for x in job.pre_run]
params['post_playbooks'] = [make_playbook(x) for x in job.post_run]
params['cleanup_playbooks'] = [make_playbook(x)
for x in job.cleanup_run]
nodes = []
for node in nodeset.getNodes():
n = node.toDict()
n.update(dict(name=node.name, label=node.label))
nodes.append(n)
params['nodes'] = nodes
params['groups'] = [group.toDict() for group in nodeset.getGroups()]
params['ssh_keys'] = []
if pipeline.post_review:
params['ssh_keys'].append(dict(
name='%s project key' % item.change.project.canonical_name,
key=item.change.project.private_ssh_key))
params['vars'] = job.combined_variables
params['extra_vars'] = job.extra_variables
params['host_vars'] = job.host_variables
params['group_vars'] = job.group_variables
params['zuul'] = zuul_params
projects = set()
required_projects = set()
def make_project_dict(project, override_branch=None,
override_checkout=None):
project_metadata = item.layout.getProjectMetadata(
project.canonical_name)
if project_metadata:
project_default_branch = project_metadata.default_branch
else:
project_default_branch = 'master'
connection = project.source.connection
return dict(connection=connection.connection_name,
name=project.name,
canonical_name=project.canonical_name,
override_branch=override_branch,
override_checkout=override_checkout,
default_branch=project_default_branch)
if job.required_projects:
for job_project in job.required_projects.values():
(trusted, project) = tenant.getProject(
job_project.project_name)
if project is None:
raise Exception("Unknown project %s" %
(job_project.project_name,))
params['projects'].append(
make_project_dict(project,
job_project.override_branch,
job_project.override_checkout))
projects.add(project)
required_projects.add(project)
for change in dependent_changes:
# We have to find the project this way because it may not
# be registered in the tenant (ie, a foreign project).
source = self.sched.connections.getSourceByCanonicalHostname(
change['project']['canonical_hostname'])
project = source.getProject(change['project']['name'])
if project not in projects:
params['projects'].append(make_project_dict(project))
projects.add(project)
for p in projects:
zuul_params['projects'][p.canonical_name] = (dict(
name=p.name,
short_name=p.name.split('/')[-1],
# Duplicate this into the dict too, so that iterating
# project.values() is easier for callers
canonical_name=p.canonical_name,
canonical_hostname=p.canonical_hostname,
src_dir=os.path.join('src', p.canonical_name),
required=(p in required_projects),
))
params['zuul_event_id'] = item.event.zuul_event_id
build = Build(job, uuid, zuul_event_id=item.event.zuul_event_id)
build.parameters = params
build.nodeset = nodeset
@ -323,7 +170,7 @@ class ExecutorClient(object):
# Update zuul attempts after addBuild above to ensure build_set
# is up to date.
attempts = build.build_set.getTries(job.name)
zuul_params['attempts'] = attempts
params["zuul"]['attempts'] = attempts
functions = getGearmanFunctions(self.gearman)
function_name = 'executor:execute'
@ -331,8 +178,9 @@ class ExecutorClient(object):
# availability zone we can get executor_zone from only the first
# node.
executor_zone = None
if nodes and nodes[0].get('attributes'):
executor_zone = nodes[0]['attributes'].get('executor-zone')
if params["nodes"] and params["nodes"][0].get('attributes'):
executor_zone = params[
"nodes"][0]['attributes'].get('executor-zone')
if executor_zone:
_fname = '%s:%s' % (

196
zuul/executor/common.py Normal file
View File

@ -0,0 +1,196 @@
# Copyright 2018 SUSE Linux GmbH
#
# 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 os
def construct_gearman_params(uuid, sched, nodeset, job, item, pipeline,
dependent_changes=[], merger_items=[],
redact_secrets_and_keys=True):
"""Returns a list of all the parameters needed to build a job.
These parameters may be passed to zuul-executors (via gearman) to perform
the job itself.
Alternatively they contain enough information to load into another build
environment - for example, a local runner.
"""
tenant = pipeline.tenant
project = dict(
name=item.change.project.name,
short_name=item.change.project.name.split('/')[-1],
canonical_hostname=item.change.project.canonical_hostname,
canonical_name=item.change.project.canonical_name,
src_dir=os.path.join('src', item.change.project.canonical_name),
)
zuul_params = dict(
build=uuid,
buildset=item.current_build_set.uuid,
ref=item.change.ref,
pipeline=pipeline.name,
post_review=pipeline.post_review,
job=job.name,
voting=job.voting,
project=project,
tenant=tenant.name,
timeout=job.timeout,
event_id=item.event.zuul_event_id if item.event else None,
jobtags=sorted(job.tags),
_inheritance_path=list(job.inheritance_path))
if job.artifact_data:
zuul_params['artifacts'] = job.artifact_data
if job.override_checkout:
zuul_params['override_checkout'] = job.override_checkout
if hasattr(item.change, 'branch'):
zuul_params['branch'] = item.change.branch
if hasattr(item.change, 'tag'):
zuul_params['tag'] = item.change.tag
if hasattr(item.change, 'number'):
zuul_params['change'] = str(item.change.number)
if hasattr(item.change, 'url'):
zuul_params['change_url'] = item.change.url
if hasattr(item.change, 'patchset'):
zuul_params['patchset'] = str(item.change.patchset)
if hasattr(item.change, 'message'):
zuul_params['message'] = item.change.message
if (hasattr(item.change, 'oldrev') and item.change.oldrev
and item.change.oldrev != '0' * 40):
zuul_params['oldrev'] = item.change.oldrev
if (hasattr(item.change, 'newrev') and item.change.newrev
and item.change.newrev != '0' * 40):
zuul_params['newrev'] = item.change.newrev
zuul_params['projects'] = {} # Set below
zuul_params['items'] = dependent_changes
zuul_params['child_jobs'] = list(item.job_graph.getDirectDependentJobs(
job.name))
params = dict()
params['job'] = job.name
params['timeout'] = job.timeout
params['post_timeout'] = job.post_timeout
params['items'] = merger_items
params['projects'] = []
if hasattr(item.change, 'branch'):
params['branch'] = item.change.branch
else:
params['branch'] = None
params['override_branch'] = job.override_branch
params['override_checkout'] = job.override_checkout
params['repo_state'] = item.current_build_set.repo_state
params['ansible_version'] = job.ansible_version
def make_playbook(playbook):
d = playbook.toDict(redact_secrets=redact_secrets_and_keys)
for role in d['roles']:
if role['type'] != 'zuul':
continue
project_metadata = item.layout.getProjectMetadata(
role['project_canonical_name'])
if project_metadata:
role['project_default_branch'] = \
project_metadata.default_branch
else:
role['project_default_branch'] = 'master'
role_trusted, role_project = item.layout.tenant.getProject(
role['project_canonical_name'])
role_connection = role_project.source.connection
role['connection'] = role_connection.connection_name
role['project'] = role_project.name
return d
if job.name != 'noop':
params['playbooks'] = [make_playbook(x) for x in job.run]
params['pre_playbooks'] = [make_playbook(x) for x in job.pre_run]
params['post_playbooks'] = [make_playbook(x) for x in job.post_run]
params['cleanup_playbooks'] = [make_playbook(x)
for x in job.cleanup_run]
nodes = []
for node in nodeset.getNodes():
n = node.toDict()
n.update(dict(name=node.name, label=node.label))
nodes.append(n)
params['nodes'] = nodes
params['groups'] = [group.toDict() for group in nodeset.getGroups()]
params['ssh_keys'] = []
if pipeline.post_review:
if redact_secrets_and_keys:
ssh_key = "REDACTED"
else:
ssh_key = item.change.project.private_ssh_key
params['ssh_keys'].append(dict(
name='%s project key' % item.change.project.canonical_name,
key=ssh_key))
params['vars'] = job.combined_variables
params['extra_vars'] = job.extra_variables
params['host_vars'] = job.host_variables
params['group_vars'] = job.group_variables
params['zuul'] = zuul_params
projects = set()
required_projects = set()
def make_project_dict(project, override_branch=None,
override_checkout=None):
project_metadata = item.layout.getProjectMetadata(
project.canonical_name)
if project_metadata:
project_default_branch = project_metadata.default_branch
else:
project_default_branch = 'master'
connection = project.source.connection
return dict(connection=connection.connection_name,
name=project.name,
canonical_name=project.canonical_name,
override_branch=override_branch,
override_checkout=override_checkout,
default_branch=project_default_branch)
if job.required_projects:
for job_project in job.required_projects.values():
(trusted, project) = tenant.getProject(
job_project.project_name)
if project is None:
raise Exception("Unknown project %s" %
(job_project.project_name,))
params['projects'].append(
make_project_dict(project,
job_project.override_branch,
job_project.override_checkout))
projects.add(project)
required_projects.add(project)
for change in dependent_changes:
# We have to find the project this way because it may not
# be registered in the tenant (ie, a foreign project).
source = sched.connections.getSourceByCanonicalHostname(
change['project']['canonical_hostname'])
project = source.getProject(change['project']['name'])
if project not in projects:
params['projects'].append(make_project_dict(project))
projects.add(project)
for p in projects:
zuul_params['projects'][p.canonical_name] = (dict(
name=p.name,
short_name=p.name.split('/')[-1],
# Duplicate this into the dict too, so that iterating
# project.values() is easier for callers
canonical_name=p.canonical_name,
canonical_hostname=p.canonical_hostname,
src_dir=os.path.join('src', p.canonical_name),
required=(p in required_projects),
))
if item.event:
params['zuul_event_id'] = item.event.zuul_event_id
return params

View File

@ -1075,11 +1075,14 @@ class PlaybookContext(ConfigObject):
if s.name not in current_names]
self.decrypted_secrets = self.decrypted_secrets + tuple(new_secrets)
def toDict(self):
def toDict(self, redact_secrets=True):
# Render to a dict to use in passing json to the executor
secrets = {}
for secret in self.decrypted_secrets:
secrets[secret.name] = secret.secret_data
if redact_secrets:
secrets[secret.name] = 'REDACTED'
else:
secrets[secret.name] = secret.secret_data
return dict(
connection=self.source_context.project.connection_name,
project=self.source_context.project.name,

View File

@ -19,6 +19,8 @@ import time
from abc import ABCMeta
from typing import List
import zuul.executor.common
from zuul import model
from zuul.connection import BaseConnection
from zuul.lib import encryption
@ -191,6 +193,7 @@ class RPCListener(RPCListenerBase):
'project_get',
'project_list',
'project_freeze_jobs',
'project_freeze_job',
'pipeline_list',
'key_get',
'config_errors_list',
@ -516,6 +519,38 @@ class RPCListener(RPCListenerBase):
gear_job.sendWorkComplete(json.dumps(output))
def handle_project_freeze_job(self, gear_job):
args = json.loads(gear_job.arguments)
tenant = self.sched.abide.tenants.get(args.get("tenant"))
project = None
pipeline = None
if tenant:
(trusted, project) = tenant.getProject(args.get("project"))
pipeline = tenant.layout.pipelines.get(args.get("pipeline"))
if not project or not pipeline:
gear_job.sendWorkComplete(json.dumps(None))
return
change = model.Branch(project)
change.branch = args.get("branch", "master")
queue = model.ChangeQueue(pipeline)
item = model.QueueItem(queue, change, None)
item.layout = tenant.layout
item.freezeJobGraph(skip_file_matcher=True)
job = item.job_graph.jobs.get(args.get("job"))
if not job:
gear_job.sendWorkComplete(json.dumps(None))
return
# TODO: check if this is frozen?
nodeset = job.nodeset
job.setBase(tenant.layout)
uuid = '0' * 32
params = zuul.executor.common.construct_gearman_params(
uuid, self.sched, nodeset,
job, item, pipeline)
gear_job.sendWorkComplete(json.dumps(params, cls=ZuulJSONEncoder))
def handle_allowed_labels_get(self, job):
args = json.loads(job.arguments)
tenant = self.sched.abide.tenants.get(args.get("tenant"))

View File

@ -1121,6 +1121,30 @@ class ZuulWebAPI(object):
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def project_freeze_job(self, tenant, pipeline, project, branch, job):
# TODO(jhesketh): Allow a canonical change/item to be passed in which
# would return the job with any in-change modifications.
job = self.rpc.submitJob(
'zuul:project_freeze_job',
{
'tenant': tenant,
'project': project,
'pipeline': pipeline,
'branch': branch,
'job': job
}
)
ret = json.loads(job.data[0])
if not ret:
raise cherrypy.HTTPError(404)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
class StaticHandler(object):
def __init__(self, root):
@ -1316,6 +1340,12 @@ class ZuulWeb(object):
'/project/{project:.*}/branch/{branch:.*}/freeze-jobs',
controller=api, action='project_freeze_jobs'
)
route_map.connect(
'api',
'/api/tenant/{tenant}/pipeline/{pipeline}'
'/project/{project:.*}/branch/{branch:.*}/freeze-job/{job}',
controller=api, action='project_freeze_job'
)
route_map.connect('api', '/api/tenant/{tenant}/pipelines',
controller=api, action='pipelines')
route_map.connect('api', '/api/tenant/{tenant}/labels',